Skip to content

Commit 411e112

Browse files
feat(split): add --before flag to insert extracted changes before split commit
1 parent 0200235 commit 411e112

File tree

4 files changed

+487
-31
lines changed

4 files changed

+487
-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: 118 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,25 @@ 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
309+
// rebased onto split
310+
// after => target is amended as "split", split is cherry picked onto split
311+
// as "extracted"
312+
313+
// FIXME terminology is wrong here: "remainder" is correct for `After` mode,
314+
// but this is actually the "extracted" commit for `InsertBefore` mode
315+
let remainder_commit_oid = if let SplitMode::InsertBefore = split_mode {
316+
repo.create_commit(
317+
None,
318+
&target_commit.get_author(),
319+
&target_commit.get_committer(),
320+
format!("temp(split): {message}").as_str(),
321+
&remainder_tree,
322+
parent_commits.iter().collect(),
323+
)?
324+
} else {
325+
target_commit.amend_commit(None, None, None, None, Some(&remainder_tree))?
326+
};
267327
let remainder_commit = repo.find_commit_or_fail(remainder_commit_oid)?;
268328

269329
if remainder_commit.is_empty() {
@@ -282,8 +342,11 @@ pub fn split(
282342
new_commit_oid: MaybeZeroOid::NonZero(remainder_commit_oid),
283343
}])?;
284344

345+
// FIXME terminology is also wrong here: "extracted" is correct for `After`
346+
// and `Discard` modes, but the extracted commit is not actually None for
347+
// `InsertBefore`; it's just handled in a different way
285348
let extracted_commit_oid = match split_mode {
286-
SplitMode::Discard => None,
349+
SplitMode::InsertBefore | SplitMode::Discard => None,
287350
SplitMode::InsertAfter | SplitMode::DetachAfter => {
288351
let extracted_tree = repo.cherry_pick_fast(
289352
&target_commit,
@@ -298,7 +361,11 @@ pub fn split(
298361
&target_commit.get_committer(),
299362
format!("temp(split): {message}").as_str(),
300363
&extracted_tree,
301-
vec![&remainder_commit],
364+
if let SplitMode::InsertBefore = &split_mode {
365+
parent_commits.iter().collect()
366+
} else {
367+
vec![&remainder_commit]
368+
},
302369
)?;
303370

304371
// see git-branchless/src/commands/amend.rs:172
@@ -358,45 +425,73 @@ pub fn split(
358425
struct CleanUp {
359426
checkout_target: Option<CheckoutTarget>,
360427
rewritten_oids: Vec<(NonZeroOid, MaybeZeroOid)>,
428+
rebase_force_detach: bool,
361429
reset_index: bool,
362430
}
363431

364432
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
433+
// branch @ target commit checked out: extend branch to include
434+
// extracted commit; branch will stay checked out w/o any explicit
435+
// checkout
367436
(TargetState::CurrentBranch, SplitMode::InsertAfter, Some(extracted_commit_oid)) => {
368437
CleanUp {
369438
checkout_target: None,
370439
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(extracted_commit_oid))],
440+
rebase_force_detach: false,
371441
reset_index: false,
372442
}
373443
}
374444

445+
// same as above, but Discard; don't move branches, but do force reset
375446
(TargetState::CurrentBranch, SplitMode::Discard, None) => CleanUp {
376447
checkout_target: None,
377448
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
449+
rebase_force_detach: false,
378450
reset_index: true,
379451
},
380452

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 {
453+
// same as above, but InsertBefore; do not move branches
454+
(TargetState::CurrentBranch, SplitMode::InsertBefore, _) => CleanUp {
455+
checkout_target: None,
456+
rewritten_oids: vec![],
457+
rebase_force_detach: false,
458+
reset_index: false,
459+
},
460+
461+
// target checked out as detached HEAD, don't extend any branches, but
462+
// explicitly check out the newly split commit
463+
(
464+
TargetState::DetachedHead,
465+
SplitMode::InsertAfter | SplitMode::Discard | SplitMode::DetachAfter,
466+
_,
467+
) => CleanUp {
384468
checkout_target: Some(CheckoutTarget::Oid(remainder_commit_oid)),
385469
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
470+
rebase_force_detach: false,
471+
reset_index: false,
472+
},
473+
474+
// same as above, but InsertBefore; do not move branches
475+
(TargetState::DetachedHead, SplitMode::InsertBefore, _) => CleanUp {
476+
checkout_target: None,
477+
rewritten_oids: vec![],
478+
rebase_force_detach: true,
386479
reset_index: false,
387480
},
388481

389482
// some other commit or branch was checked out, default behavior is fine
390483
(TargetState::CurrentBranch | TargetState::Other, _, _) => CleanUp {
391484
checkout_target: None,
392485
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
486+
rebase_force_detach: false,
393487
reset_index: false,
394488
},
395489
};
396490

397491
let CleanUp {
398492
checkout_target,
399493
rewritten_oids,
494+
rebase_force_detach,
400495
reset_index,
401496
} = cleanup;
402497

@@ -431,21 +526,18 @@ pub fn split(
431526
}
432527

433528
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])?
529+
if let SplitMode::InsertBefore = &split_mode {
530+
builder.move_subtree(target_oid, vec![remainder_commit_oid])?
531+
} else {
532+
let children = dag.query_children(CommitSet::from(target_oid))?;
533+
for child in dag.commit_set_to_vec(&children)? {
534+
match (&split_mode, extracted_commit_oid) {
535+
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
536+
builder.move_subtree(child, vec![remainder_commit_oid])?
537+
}
538+
(_, Some(extracted_commit_oid)) => {
539+
builder.move_subtree(child, vec![extracted_commit_oid])?
540+
}
449541
}
450542
}
451543
}
@@ -466,7 +558,7 @@ pub fn split(
466558
resolve_merge_conflicts,
467559
check_out_commit_options: CheckOutCommitOptions {
468560
additional_args: Default::default(),
469-
force_detach: false,
561+
force_detach: rebase_force_detach,
470562
reset: false,
471563
render_smartlog: false,
472564
},

0 commit comments

Comments
 (0)