Skip to content

Commit 6ec7f67

Browse files
feat(split): add --before flag to insert extracted changes before split commit
1 parent f5a760b commit 6ec7f67

File tree

4 files changed

+482
-31
lines changed

4 files changed

+482
-31
lines changed

git-branchless-opts/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,10 @@ pub enum Command {
662662
#[clap(value_parser, required = true)]
663663
files: Vec<String>,
664664

665+
/// Insert the extracted commit before (as a parent of) the split commit.
666+
#[clap(action, short = 'b', long)]
667+
before: bool,
668+
665669
/// Restack any descendents onto the split commit, not the extracted commit.
666670
#[clap(action, short = 'd', long)]
667671
detach: bool,

git-branchless/src/commands/mod.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,23 @@ fn command_main(ctx: CommandContext, opts: Opts) -> EyreExitOr<()> {
181181
},
182182

183183
Command::Split {
184+
before,
184185
detach,
185186
discard,
186187
files,
187188
resolve_revset_options,
188189
revset,
189190
move_options,
190191
} => {
191-
let split_mode = match (detach, discard) {
192-
(true, false) => split::SplitMode::DetachAfter,
193-
(false, true) => split::SplitMode::Discard,
194-
(false, false) => split::SplitMode::InsertAfter,
195-
(true, true) => {
192+
let split_mode = match (before, detach, discard) {
193+
(false, true, false) => split::SplitMode::DetachAfter,
194+
(false, false, true) => split::SplitMode::Discard,
195+
(false, false, false) => split::SplitMode::InsertAfter,
196+
(true, false, false) => split::SplitMode::InsertBefore,
197+
(true, true, false)
198+
| (true, false, true)
199+
| (false, true, true)
200+
| (true, true, true) => {
196201
unreachable!("clap should prevent this")
197202
}
198203
};

git-branchless/src/commands/split.rs

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub enum SplitMode {
4040
DetachAfter,
4141
Discard,
4242
InsertAfter,
43+
InsertBefore,
4344
}
4445

4546
/// Split a commit and restack its descendants.
@@ -123,6 +124,26 @@ pub fn split(
123124
}
124125
};
125126

127+
//
128+
// a-t-b
129+
//
130+
// a-r-x-b (default)
131+
// a-x-r-b (before)
132+
// a-r-b (detach)
133+
// \-x
134+
// a-r-b (discard)
135+
//
136+
// default: x == t tree, x is t with changes removed
137+
// before: r == t tree, e is a with changes added
138+
// detach: (same as default, different rebase)
139+
// discard: (same as default, w/o any rebase)
140+
//
141+
// below:
142+
// a => parent
143+
// t => target
144+
// r => remainder
145+
// x => extracted
146+
126147
let target_commit = repo.find_commit_or_fail(target_oid)?;
127148
let target_tree = target_commit.get_tree()?;
128149
let parent_commits = target_commit.get_parents();
@@ -135,11 +156,20 @@ pub fn split(
135156
(only_parent.get_tree()?, target_commit.get_tree()?)
136157
}
137158

159+
// split the commit by adding the changed to a copy of the parent tree,
160+
// then rebasing the orignal target onto the extracted commit
161+
(SplitMode::InsertBefore, [only_parent]) => {
162+
(only_parent.get_tree()?, only_parent.get_tree()?)
163+
}
164+
138165
// no parent: use an empty tree for comparison
139166
(SplitMode::InsertAfter, []) | (SplitMode::Discard, []) | (SplitMode::DetachAfter, []) => {
140167
(make_empty_tree(&repo)?, target_commit.get_tree()?)
141168
}
142169

170+
// no parent: add extracted changes to an empty tree
171+
(SplitMode::InsertBefore, []) => (make_empty_tree(&repo)?, make_empty_tree(&repo)?),
172+
143173
(_, [..]) => {
144174
writeln!(
145175
effects.get_error_stream(),
@@ -220,6 +250,15 @@ pub fn split(
220250

221251
let target_entry = target_tree.get_path(path)?;
222252
let temp_tree_oid = match (parent_entry, target_entry, &split_mode) {
253+
// added or modified & InsertBefore => add to extracted commit
254+
(None, Some(commit_entry), SplitMode::InsertBefore)
255+
| (Some(_), Some(commit_entry), SplitMode::InsertBefore) => {
256+
remainder_tree.add_or_replace(&repo, path, &commit_entry)?
257+
}
258+
259+
// removed & InsertBefore => remove from remainder commit
260+
(Some(_), None, SplitMode::InsertBefore) => remainder_tree.remove(&repo, path)?,
261+
223262
// added => remove from remainder commit
224263
(None, Some(_), SplitMode::InsertAfter)
225264
| (None, Some(_), SplitMode::DetachAfter)
@@ -251,7 +290,11 @@ pub fn split(
251290
.expect("should have been found");
252291
}
253292
let message = {
254-
let (old_tree, new_tree) = (&remainder_tree, &target_tree);
293+
let (old_tree, new_tree) = if let SplitMode::InsertBefore = &split_mode {
294+
(&parent_tree, &remainder_tree)
295+
} else {
296+
(&remainder_tree, &target_tree)
297+
};
255298
let diff = repo.get_diff_between_trees(
256299
effects,
257300
Some(old_tree),
@@ -262,8 +305,23 @@ pub fn split(
262305
summarize_diff_for_temporary_commit(&diff)?
263306
};
264307

265-
let remainder_commit_oid =
266-
target_commit.amend_commit(None, None, None, None, Some(&remainder_tree))?;
308+
// before => split commit is created on parent as "extracted", target is rebased onto split
309+
// after => target is amended as "split", split is cherry picked onto split as "extracted"
310+
311+
// FIXME terminology is wrong here: remainder is correct for "After", but
312+
// this is the "extracted" commit for InsertBefore
313+
let remainder_commit_oid = if let SplitMode::InsertBefore = split_mode {
314+
repo.create_commit(
315+
None,
316+
&target_commit.get_author(),
317+
&target_commit.get_committer(),
318+
format!("temp(split): {message}").as_str(),
319+
&remainder_tree,
320+
parent_commits.iter().collect(),
321+
)?
322+
} else {
323+
target_commit.amend_commit(None, None, None, None, Some(&remainder_tree))?
324+
};
267325
let remainder_commit = repo.find_commit_or_fail(remainder_commit_oid)?;
268326

269327
if remainder_commit.is_empty() {
@@ -282,8 +340,11 @@ pub fn split(
282340
new_commit_oid: MaybeZeroOid::NonZero(remainder_commit_oid),
283341
}])?;
284342

343+
// FIXME terminology is wrong here: extracted is correct for After and
344+
// Discard modes, but the extracted commit is not None for InsertBefore:
345+
// it's just handled in a different way
285346
let extracted_commit_oid = match split_mode {
286-
SplitMode::Discard => None,
347+
SplitMode::InsertBefore | SplitMode::Discard => None,
287348
SplitMode::InsertAfter | SplitMode::DetachAfter => {
288349
let extracted_tree = repo.cherry_pick_fast(
289350
&target_commit,
@@ -298,7 +359,11 @@ pub fn split(
298359
&target_commit.get_committer(),
299360
format!("temp(split): {message}").as_str(),
300361
&extracted_tree,
301-
vec![&remainder_commit],
362+
if let SplitMode::InsertBefore = &split_mode {
363+
parent_commits.iter().collect()
364+
} else {
365+
vec![&remainder_commit]
366+
},
302367
)?;
303368

304369
// see git-branchless/src/commands/amend.rs:172
@@ -358,45 +423,73 @@ pub fn split(
358423
struct CleanUp {
359424
checkout_target: Option<CheckoutTarget>,
360425
rewritten_oids: Vec<(NonZeroOid, MaybeZeroOid)>,
426+
rebase_force_detach: bool,
361427
reset_index: bool,
362428
}
363429

364430
let cleanup = match (target_state, &split_mode, extracted_commit_oid) {
365-
// branch @ split commit checked out: extend branch to include extracted
366-
// commit; branch will stay checked out w/o any explicit checkout
431+
// branch @ target commit checked out: extend branch to include
432+
// extracted commit; branch will stay checked out w/o any explicit
433+
// checkout
367434
(TargetState::CurrentBranch, SplitMode::InsertAfter, Some(extracted_commit_oid)) => {
368435
CleanUp {
369436
checkout_target: None,
370437
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(extracted_commit_oid))],
438+
rebase_force_detach: false,
371439
reset_index: false,
372440
}
373441
}
374442

443+
// same as above, but Discard; don't move branches, but do force reset
375444
(TargetState::CurrentBranch, SplitMode::Discard, None) => CleanUp {
376445
checkout_target: None,
377446
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
447+
rebase_force_detach: false,
378448
reset_index: true,
379449
},
380450

381-
// commit to split checked out as detached HEAD, don't extend any
382-
// branches, but explicitly check out the newly split commit
383-
(TargetState::DetachedHead, _, _) => CleanUp {
451+
// same as above, but InsertBefore; do not move branches
452+
(TargetState::CurrentBranch, SplitMode::InsertBefore, _) => CleanUp {
453+
checkout_target: None,
454+
rewritten_oids: vec![],
455+
rebase_force_detach: false,
456+
reset_index: false,
457+
},
458+
459+
// target checked out as detached HEAD, don't extend any branches, but
460+
// explicitly check out the newly split commit
461+
(
462+
TargetState::DetachedHead,
463+
SplitMode::InsertAfter | SplitMode::Discard | SplitMode::DetachAfter,
464+
_,
465+
) => CleanUp {
384466
checkout_target: Some(CheckoutTarget::Oid(remainder_commit_oid)),
385467
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
468+
rebase_force_detach: false,
469+
reset_index: false,
470+
},
471+
472+
// same as above, but InsertBefore; do not move branches
473+
(TargetState::DetachedHead, SplitMode::InsertBefore, _) => CleanUp {
474+
checkout_target: None,
475+
rewritten_oids: vec![],
476+
rebase_force_detach: true,
386477
reset_index: false,
387478
},
388479

389480
// some other commit or branch was checked out, default behavior is fine
390481
(TargetState::CurrentBranch | TargetState::Other, _, _) => CleanUp {
391482
checkout_target: None,
392483
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
484+
rebase_force_detach: false,
393485
reset_index: false,
394486
},
395487
};
396488

397489
let CleanUp {
398490
checkout_target,
399491
rewritten_oids,
492+
rebase_force_detach,
400493
reset_index,
401494
} = cleanup;
402495

@@ -431,21 +524,18 @@ pub fn split(
431524
}
432525

433526
let mut builder = RebasePlanBuilder::new(&dag, permissions);
434-
let children = dag.query_children(CommitSet::from(target_oid))?;
435-
for child in dag.commit_set_to_vec(&children)? {
436-
match (&split_mode, extracted_commit_oid) {
437-
(_, None) => builder.move_subtree(child, vec![remainder_commit_oid])?,
438-
(_, Some(extracted_commit_oid)) => {
439-
builder.move_subtree(child, vec![extracted_commit_oid])?
440-
}
441-
}
442-
443-
match (&split_mode, extracted_commit_oid) {
444-
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
445-
builder.move_subtree(child, vec![remainder_commit_oid])?
446-
}
447-
(_, Some(extracted_commit_oid)) => {
448-
builder.move_subtree(child, vec![extracted_commit_oid])?
527+
if let SplitMode::InsertBefore = &split_mode {
528+
builder.move_subtree(target_oid, vec![remainder_commit_oid])?
529+
} else {
530+
let children = dag.query_children(CommitSet::from(target_oid))?;
531+
for child in dag.commit_set_to_vec(&children)? {
532+
match (&split_mode, extracted_commit_oid) {
533+
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
534+
builder.move_subtree(child, vec![remainder_commit_oid])?
535+
}
536+
(_, Some(extracted_commit_oid)) => {
537+
builder.move_subtree(child, vec![extracted_commit_oid])?
538+
}
449539
}
450540
}
451541
}
@@ -466,7 +556,7 @@ pub fn split(
466556
resolve_merge_conflicts,
467557
check_out_commit_options: CheckOutCommitOptions {
468558
additional_args: Default::default(),
469-
force_detach: false,
559+
force_detach: rebase_force_detach,
470560
reset: false,
471561
render_smartlog: false,
472562
},

0 commit comments

Comments
 (0)