Skip to content

Commit 2c69e0d

Browse files
committed
Use but-rebase to rewrite the history up to tips of workspaces.
Additionally, provide the information needed to rewrite hidden/internal references.
1 parent 518936a commit 2c69e0d

File tree

14 files changed

+204
-28
lines changed

14 files changed

+204
-28
lines changed

Cargo.lock

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but-core/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub enum UnifiedDiff {
9494
}
9595

9696
/// Either git reference or a virtual reference (i.e. a reference not visible in Git).
97-
#[derive(Debug, Clone)]
97+
#[derive(Debug, Clone, PartialEq)]
9898
pub enum Reference {
9999
/// A git reference or lightweight tag.
100100
Git(gix::refs::PartialName),

crates/but-debugging/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ publish = false
99
doctest = false
1010

1111
[dependencies]
12+
gix.workspace = true
1213

1314
[dev-dependencies]

crates/but-debugging/src/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
use gix::bstr::ByteSlice;
12
/// But Debugging contains utilities that aid in debugging gitbutler.
23
/// Utilities defined inside of but-debugging should not be relied upon in
34
/// tests or used in production code.
45
use std::{path::Path, process::Command};
56

7+
/// Produce a graph of all commits reachable from `refspec`.
8+
pub fn commit_graph(repo: &gix::Repository, refspec: impl ToString) -> std::io::Result<String> {
9+
let log = std::process::Command::new(gix::path::env::exe_invocation())
10+
.current_dir(repo.path())
11+
.args(["log", "--oneline", "--graph", "--decorate"])
12+
.arg(refspec.to_string())
13+
.output()?;
14+
assert!(log.status.success());
15+
Ok(log.stdout.to_str().expect("no illformed UTF-8").to_string())
16+
}
17+
618
/// Options passed to `git log`
719
pub struct LogOptions {
820
/// Controls whether the `--oneline` flag is passed.

crates/but-rebase/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ tempfile.workspace = true
2222

2323
[dev-dependencies]
2424
gix-testtools.workspace = true
25+
but-debugging.workspace = true
2526
gix = {workspace = true, features = ["revision"]}
2627
insta = "1.42.1"
2728
but-core = { workspace = true, features = ["testing"] }

crates/but-rebase/src/lib.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! An API for an interactive rebase.
1+
//! An API for an interactive rebases, suitable for interactive, UI driven, and programmatic use.
22
#![deny(rust_2018_idioms, missing_docs)]
33

44
use anyhow::{anyhow, bail, Ok, Result};
@@ -19,6 +19,9 @@ pub enum RebaseStep {
1919
commit_id: gix::ObjectId,
2020
/// Optional message to use for newly produced commit
2121
new_message: Option<BString>,
22+
// TODO: add `base: Option<ObjectId>` to allow restarting the sequence at a new base
23+
// for multi-branch rebasing. It would keep the previous cursor, to allow the last
24+
// branch to contain a pick of the merge commit on top, which it can then correctly re-merge.
2225
},
2326
/// Merge an existing commit and it's parents producing a new merge commit.
2427
Merge {

crates/but-rebase/tests/rebase/main.rs

+2-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use crate::utils::{assure_stable_env, commit_graph, fixture_writable, four_commits_writable};
1+
use crate::utils::{assure_stable_env, fixture_writable, four_commits_writable};
22
use anyhow::Result;
3+
use but_debugging::commit_graph;
34
use but_rebase::{RebaseBuilder, RebaseStep};
45

56
mod error_handling;
@@ -144,20 +145,8 @@ fn pick_the_first_commit_with_no_parents_for_squashing() -> Result<()> {
144145

145146
pub mod utils {
146147
use anyhow::Result;
147-
use bstr::ByteSlice;
148148
use gix::ObjectId;
149149

150-
/// Produce a graph of all commits reachable from `refspec`.
151-
pub fn commit_graph(repo: &gix::Repository, refspec: impl ToString) -> Result<String> {
152-
let log = std::process::Command::new(gix::path::env::exe_invocation())
153-
.current_dir(repo.path())
154-
.args(["log", "--oneline", "--graph", "--decorate"])
155-
.arg(refspec.to_string())
156-
.output()?;
157-
assert!(log.status.success());
158-
Ok(log.stdout.to_str().expect("no illformed UTF-8").to_string())
159-
}
160-
161150
/// Returns a fixture that may not be written to, objects will never touch disk either.
162151
pub fn fixture(fixture_name: &str) -> Result<gix::Repository> {
163152
let root = gix_testtools::scripted_fixture_read_only("rebase.sh")

crates/but-workspace/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ url = { version = "2.5.4", features = ["serde"] }
2828
md5 = "0.7.0"
2929

3030
[dev-dependencies]
31+
but-debugging.workspace = true
3132
gix-testtools = "0.15.0"
3233
gitbutler-testsupport.workspace = true
3334
insta = "1.42.1"

crates/but-workspace/src/commit_engine/mod.rs

+114-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
//! The machinery used to alter and mutate commits in various ways.
1+
//! The machinery used to alter and mutate commits in various ways whilst adjusting descendant commits within a [reference frame](ReferenceFrame).
22
33
use anyhow::{bail, Context};
44
use bstr::{BString, ByteSlice};
55
use but_core::unified_diff::DiffHunk;
66
use but_core::{RepositoryExt, UnifiedDiff};
7+
use gitbutler_stack::VirtualBranchesState;
78
use gix::filter::plumbing::driver::apply::{Delay, MaybeDelayed};
89
use gix::filter::plumbing::pipeline::convert::{ToGitOutcome, ToWorktreeOutcome};
910
use gix::merge::tree::TreatAsUnresolved;
1011
use gix::objs::tree::EntryKind;
1112
use gix::prelude::ObjectIdExt;
13+
use gix::refs::transaction::PreviousValue;
1214
use serde::{Deserialize, Serialize};
1315
use std::io::Read;
1416

@@ -35,6 +37,35 @@ pub enum Destination {
3537
AmendCommit(gix::ObjectId),
3638
}
3739

40+
/// Quite literally a set of commits that surround the commit to be altered or created to help finding descendant
41+
/// commits that should be rewritten to integrate the new commit.
42+
///
43+
/// Or explained differently, a set of commits that helps to navigate the commit graph to steer the necessary
44+
/// [rewrites](but_rebase::RebaseBuilder::rebase()), keeping them linear.
45+
#[derive(Debug, Clone)]
46+
pub struct ReferenceFrame {
47+
/// The commit at the top of the branch which has the parent of the new commit or the amended commit
48+
/// in its ancestry.
49+
pub branch_tip: gix::ObjectId,
50+
/// If present, the merge commit that integrates all branches into one workspace.
51+
/// Must have the [`branch_tip`](Self::branch_tip) as direct ancestor/parent.
52+
pub workspace_tip: Option<gix::ObjectId>,
53+
/// A set of literal references along with the commit they point to, ideally (but not necessarily) filtered to only
54+
/// the commits we would have to rebase.
55+
/// If a reference is unborn, it can't be listed here and needs special treatment.
56+
pub references: Vec<(but_core::Reference, gix::ObjectId)>,
57+
}
58+
59+
/// Identify the commit that contains the patches to be moved, along with the branch that should be rewritten.
60+
#[derive(Debug, Clone, Copy)]
61+
pub struct MoveSourceCommit {
62+
/// The commit that acts as the source of all changes. Note that these changes will be *removed* from the
63+
/// commit, which gets rewritten in the process.
64+
pub commit_id: gix::ObjectId,
65+
/// The commit at the top of the branch which has the commit that acts as source of changes in its ancestry.
66+
pub branch_tip: gix::ObjectId,
67+
}
68+
3869
/// A change that should be used to create a new commit or alter an existing one, along with enough information to know where to find it.
3970
#[derive(Debug, Default, Clone, PartialEq)]
4071
pub struct DiffSpec {
@@ -93,6 +124,10 @@ pub struct CreateCommitOutcome {
93124
pub rejected_specs: Vec<DiffSpec>,
94125
/// The newly created commit, or `None` if no commit could be created as all changes-requests were rejected.
95126
pub new_commit: Option<gix::ObjectId>,
127+
/// The rewritten references `(old, new, reference)`, along with their `old` and `new` commit location, along
128+
/// with the reference itself.
129+
/// If `new_commit` is `None`, this array will be an empty.
130+
pub references: Vec<(gix::ObjectId, gix::ObjectId, but_core::Reference)>,
96131
}
97132

98133
/// Additional information about the outcome of a [`create_tree()`] call.
@@ -102,15 +137,16 @@ pub struct CreateTreeOutcome {
102137
/// when merging the workspace commit, or because the specified hunks didn't match exactly due to changes
103138
/// that happened in the meantime, or if a file without a change was specified.
104139
pub rejected_specs: Vec<DiffSpec>,
105-
/// The newly created tree, or `None` if no commit could be created as all changes-requests were rejected.
106-
pub new_tree: Option<gix::ObjectId>,
140+
/// The newly created tree that acts as the destination of the changes, or `None` if no commit could be
141+
/// created as all changes-requests were rejected.
142+
pub destination_tree: Option<gix::ObjectId>,
107143
}
108144

109145
/// Like [`create_commit()`], but lower-level and only returns a new tree, without finally associating it with a commit.
110146
pub fn create_tree(
111147
repo: &gix::Repository,
112148
destination: &Destination,
113-
origin_commit: Option<gix::ObjectId>,
149+
move_source: Option<MoveSourceCommit>,
114150
changes: Vec<DiffSpec>,
115151
context_lines: u32,
116152
) -> anyhow::Result<CreateTreeOutcome> {
@@ -136,7 +172,7 @@ pub fn create_tree(
136172

137173
let mut changes: Vec<_> = changes.into_iter().map(Ok).collect();
138174
let new_tree = 'retry: loop {
139-
let (maybe_new_tree, actual_base_tree) = if let Some(_origin_commit) = origin_commit {
175+
let (maybe_new_tree, actual_base_tree) = if let Some(_source) = move_source {
140176
todo!("get base tree and apply changes by cherry-picking, probably can all be done by one function, but optimizations are different")
141177
} else {
142178
let changes_base_tree = repo.head()?.id().and_then(|id| {
@@ -196,25 +232,25 @@ pub fn create_tree(
196232
};
197233
Ok(CreateTreeOutcome {
198234
rejected_specs: changes.into_iter().filter_map(Result::err).collect(),
199-
new_tree,
235+
destination_tree: new_tree,
200236
})
201237
}
202238

203239
/// Alter the single `destination` in a given `frame` with as many `changes` as possible and write new objects into `repo`,
204240
/// but only if the commit succeeds.
205-
/// If `origin_commit` is `Some(commit)`, all changes are considered to originate from the given commit, otherwise they originate from the worktree.
241+
///
242+
/// If `move_source` is `Some(source)`, all changes are considered to originate from the given commit to move out of, otherwise they originate from the worktree.
206243
/// `context_lines` is the amount of lines of context included in each [`HunkHeader`], and the value that will be used to recover the existing hunks,
207244
/// so that the hunks can be matched.
208245
///
209246
/// Return additional information that helps to understand to what extent the commit was created, as the commit might not contain all the [`DiffSpecs`](DiffSpec)
210247
/// that were requested if they failed to apply.
211248
///
212-
/// Note that the ref pointed to by `HEAD` will be updated with the new commit if the new commits parent was pointed to by `HEAD` before. Detached heads will cause failure.
213-
/// If `allow_ref_change` is false, `HEAD` will never be adjusted to prevent additional side-effects.
249+
/// No reference is touched in the process.
214250
pub fn create_commit(
215251
repo: &gix::Repository,
216252
destination: Destination,
217-
origin_commit: Option<gix::ObjectId>,
253+
move_source: Option<MoveSourceCommit>,
218254
changes: Vec<DiffSpec>,
219255
context_lines: u32,
220256
) -> anyhow::Result<CreateCommitOutcome> {
@@ -242,9 +278,9 @@ pub fn create_commit(
242278

243279
let CreateTreeOutcome {
244280
rejected_specs,
245-
new_tree,
246-
} = create_tree(repo, &destination, origin_commit, changes, context_lines)?;
247-
let new_commit = if let Some(new_tree) = new_tree {
281+
destination_tree,
282+
} = create_tree(repo, &destination, move_source, changes, context_lines)?;
283+
let new_commit = if let Some(new_tree) = destination_tree {
248284
match destination {
249285
Destination::NewCommit {
250286
message,
@@ -273,9 +309,74 @@ pub fn create_commit(
273309
Ok(CreateCommitOutcome {
274310
rejected_specs,
275311
new_commit,
312+
references: Vec::new(),
276313
})
277314
}
278315

316+
/// Like [`create_commit()`], but allows to also update virtual branches and git references pointing to commits
317+
///
318+
/// `frame` is used to rebase all descendants to rewrite the relevant history to include the new or amended commit.
319+
pub fn create_commit_and_update_refs(
320+
repo: &gix::Repository,
321+
branches: &mut VirtualBranchesState,
322+
destination: Destination,
323+
move_source: Option<MoveSourceCommit>,
324+
changes: Vec<DiffSpec>,
325+
context_lines: u32,
326+
) -> anyhow::Result<CreateCommitOutcome> {
327+
let mut out = create_commit(
328+
repo,
329+
destination.clone(),
330+
move_source,
331+
changes,
332+
context_lines,
333+
)?;
334+
335+
// TODO: if move_source is used, we will have another field here.
336+
let CreateCommitOutcome {
337+
rejected_specs,
338+
new_commit,
339+
references,
340+
} = &mut out;
341+
342+
let Some(new_commit) = new_commit else {
343+
return Ok(out);
344+
};
345+
346+
let commit_to_find = match destination {
347+
Destination::NewCommit {
348+
parent_commit_id, ..
349+
} => parent_commit_id,
350+
Destination::AmendCommit(commit) => Some(commit),
351+
};
352+
353+
if let Some(destination_commit) = commit_to_find {
354+
// Traverse all stacks in the workspace and find the one that contains the destination commit
355+
// branches.list_stacks_in_workspace()
356+
// but_rebase::RebaseBuilder::new()?.step
357+
todo!("rewrite existing commits")
358+
} else {
359+
// unborn branch special case.
360+
repo.reference(
361+
repo.head_name()?
362+
.context("unborn HEAD must contain a ref-name")?,
363+
*new_commit,
364+
PreviousValue::Any,
365+
format!(
366+
"commit (initial): {title}",
367+
title = new_commit
368+
.attach(repo)
369+
.object()?
370+
.into_commit()
371+
.message()?
372+
.title
373+
),
374+
)?;
375+
}
376+
377+
Ok(out)
378+
}
379+
279380
fn into_err_spec(input: &mut PossibleChange) {
280381
*input = match std::mem::replace(input, Ok(Default::default())) {
281382
// What we thought was a good change turned out to be a no-op, rejected.

crates/but-workspace/src/commit_engine/ui.rs

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ impl From<super::CreateCommitOutcome> for CreateCommitOutcome {
5050
super::CreateCommitOutcome {
5151
rejected_specs,
5252
new_commit,
53+
references: _,
5354
}: super::CreateCommitOutcome,
5455
) -> Self {
5556
CreateCommitOutcome {

crates/but-workspace/tests/workspace/commit_engine/amend_commit.rs

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn all_changes_and_renames_to_topmost_commit_no_parent() -> anyhow::Result<()> {
2727
new_commit: Some(
2828
Sha1(aacf6391c96a59461df0a241caad4ad24368f542),
2929
),
30+
references: [],
3031
}
3132
");
3233
let tree = visualize_tree(&repo, &outcome)?;
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
mod amend_commit;
22
mod new_commit;
3+
mod refs_update;
34
mod utils;

0 commit comments

Comments
 (0)