Skip to content

Commit 1b93c7a

Browse files
fix(move): Support on-disk rebases with move --fixup
1 parent 338f5b3 commit 1b93c7a

File tree

5 files changed

+585
-12
lines changed

5 files changed

+585
-12
lines changed

git-branchless-hook/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,9 +576,16 @@ pub fn command_main(ctx: CommandContext, args: HookArgs) -> EyreExitOr<()> {
576576
hook_register_extra_post_rewrite_hook()?;
577577
}
578578

579-
HookSubcommand::SkipUpstreamAppliedCommit { commit_oid } => {
579+
HookSubcommand::SkipUpstreamAppliedCommit {
580+
commit_oid,
581+
rewritten_oid,
582+
} => {
580583
let commit_oid: NonZeroOid = commit_oid.parse()?;
581-
hook_skip_upstream_applied_commit(&effects, commit_oid)?;
584+
let rewritten_oid: MaybeZeroOid = match rewritten_oid {
585+
Some(rewritten_oid) => rewritten_oid.parse()?,
586+
None => MaybeZeroOid::Zero,
587+
};
588+
hook_skip_upstream_applied_commit(&effects, commit_oid, rewritten_oid)?;
582589
}
583590
}
584591

git-branchless-lib/src/core/rewrite/plan.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::sync::Arc;
66

77
use chashmap::CHashMap;
88
use eyre::Context;
9-
use itertools::Itertools;
9+
use itertools::{intersperse, Itertools};
1010
use rayon::{prelude::*, ThreadPool};
1111
use tracing::{instrument, warn};
1212

@@ -148,7 +148,63 @@ impl ToString for RebaseCommand {
148148
} => match commits_to_apply_oids.as_slice() {
149149
[] => String::new(),
150150
[commit_oid] => format!("pick {commit_oid}"),
151-
[..] => unimplemented!("On-disk fixups"),
151+
[pick_oid, fixup_oids @ ..] => {
152+
let mut picks = vec![format!("pick {pick_oid}")];
153+
let mut fixups = fixup_oids
154+
.iter()
155+
.map(|oid| format!("fixup {oid}"))
156+
.collect::<Vec<String>>();
157+
let mut cleanups = vec![];
158+
159+
// Since 0ca8681, the intermediate commits created as each
160+
// fixup is applied are left behind in the smartlog. This
161+
// forcibly removes all but the last of them. I don't
162+
// understand why this happens during `git branchless`
163+
// initiated rebases, but not during "normal" fixup rebases,
164+
// but this makes these artifacts go away.
165+
if fixups.len() > 1 {
166+
fixups = intersperse(
167+
fixups,
168+
"exec git branchless hook-skip-upstream-applied-commit $(git rev-parse HEAD)".to_string()
169+
).collect()
170+
}
171+
172+
// If the destination commit (ie `original_commit_oid`) does
173+
// not come first topologically among the commits being
174+
// rebased, then the final squashed commit will end up with
175+
// the wrong metadata. (It will end up with the metadata
176+
// from the commit that *does* come first, ie `pick_oid`.)
177+
// We have to add some additional steps to make sure the
178+
// smartlog and commit metadata are left as the user
179+
// expects.
180+
if pick_oid != original_commit_oid {
181+
// See above comment related to 0ca8681
182+
picks.insert(
183+
1,
184+
"exec git branchless hook-skip-upstream-applied-commit $(git rev-parse HEAD)".to_string()
185+
);
186+
187+
cleanups = vec![
188+
// Hide the final squashed commit
189+
"exec git branchless hook-skip-upstream-applied-commit $(git rev-parse HEAD)".to_string(),
190+
191+
// Create a new final commit by applying the
192+
// metadata from the destination commit to the just
193+
// now hidden commit.
194+
format!("exec git commit --amend --no-edit --reuse-message {original_commit_oid}"),
195+
196+
// Finally, register the new final commit as the
197+
// rewritten version of original_commit_oid
198+
format!("exec git branchless hook-skip-upstream-applied-commit {original_commit_oid} $(git rev-parse HEAD)")
199+
];
200+
}
201+
202+
picks
203+
.iter()
204+
.chain(fixups.iter())
205+
.chain(cleanups.iter())
206+
.join("\n")
207+
}
152208
},
153209
RebaseCommand::Merge {
154210
commit_oid,
@@ -1170,9 +1226,16 @@ impl<'a> RebasePlanBuilder<'a> {
11701226

11711227
let first_parent_oid = *parent_oids.first().unwrap();
11721228
first_dest_oid.get_or_insert(first_parent_oid);
1173-
acc.push(RebaseCommand::Reset {
1174-
target: OidOrLabel::Oid(first_parent_oid),
1175-
});
1229+
// FIXME This feels heavy handed, but seems to be necessary for some fixup cases.
1230+
if !state
1231+
.constraints
1232+
.commits_to_move()
1233+
.contains(&first_parent_oid)
1234+
{
1235+
acc.push(RebaseCommand::Reset {
1236+
target: OidOrLabel::Oid(first_parent_oid),
1237+
});
1238+
}
11761239

11771240
let upstream_patch_ids = if *detect_duplicate_commits_via_patch_id {
11781241
let (effects, _progress) =

git-branchless-lib/src/core/rewrite/rewrite_hooks.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,11 +516,14 @@ pub fn hook_drop_commit_if_empty(
516516
Ok(())
517517
}
518518

519-
/// For rebases, if a commit is known to have been applied upstream, skip it
520-
/// without attempting to apply it.
519+
/// For rebases, update the status of a commit that is known to have been
520+
/// applied upstream. It can either be skipped entirely (when called with
521+
/// `MaybeZeroOid::Zero`) or be marked as having been rewritten to a
522+
/// different commit entirely.
521523
pub fn hook_skip_upstream_applied_commit(
522524
effects: &Effects,
523525
commit_oid: NonZeroOid,
526+
rewritten_oid: MaybeZeroOid,
524527
) -> eyre::Result<()> {
525528
let repo = Repo::from_current_dir()?;
526529
let commit = repo.find_commit_or_fail(commit_oid)?;
@@ -546,7 +549,7 @@ pub fn hook_skip_upstream_applied_commit(
546549
add_rewritten_list_entries(
547550
&repo.get_tempfile_dir(),
548551
&repo.get_rebase_state_dir_path().join("rewritten-list"),
549-
&[(commit_oid, MaybeZeroOid::Zero)],
552+
&[(commit_oid, rewritten_oid)],
550553
)?;
551554

552555
Ok(())

git-branchless-opts/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ pub enum HookSubcommand {
232232
/// The OID of the commit that was skipped.
233233
#[clap(value_parser)]
234234
commit_oid: String,
235+
236+
/// The OID of the commit that was skipped (if Some) or removed (if None).
237+
#[clap(value_parser)]
238+
rewritten_oid: Option<String>,
235239
},
236240
}
237241

0 commit comments

Comments
 (0)