Skip to content

Commit 77d3d29

Browse files
feat(split): add --before flag to insert extracted changes before split commit
1 parent 1f58b75 commit 77d3d29

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)
@@ -253,7 +292,11 @@ pub fn split(
253292
let message = {
254293
let (effects, _progress) =
255294
effects.start_operation(lib::core::effects::OperationType::CalculateDiff);
256-
let (old_tree, new_tree) = (&remainder_tree, &target_tree);
295+
let (old_tree, new_tree) = if let SplitMode::InsertBefore = &split_mode {
296+
(&parent_tree, &remainder_tree)
297+
} else {
298+
(&remainder_tree, &target_tree)
299+
};
257300
let diff = repo.get_diff_between_trees(
258301
&effects,
259302
Some(old_tree),
@@ -264,8 +307,23 @@ pub fn split(
264307
summarize_diff_for_temporary_commit(&diff)?
265308
};
266309

267-
let remainder_commit_oid =
268-
target_commit.amend_commit(None, None, None, None, Some(&remainder_tree))?;
310+
// before => split commit is created on parent as "extracted", target is rebased onto split
311+
// after => target is amended as "split", split is cherry picked onto split as "extracted"
312+
313+
// FIXME terminology is wrong here: remainder is correct for "After", but
314+
// this is the "extracted" commit for InsertBefore
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+
};
269327
let remainder_commit = repo.find_commit_or_fail(remainder_commit_oid)?;
270328

271329
if remainder_commit.is_empty() {
@@ -284,8 +342,11 @@ pub fn split(
284342
new_commit_oid: MaybeZeroOid::NonZero(remainder_commit_oid),
285343
}])?;
286344

345+
// FIXME terminology is wrong here: extracted is correct for After and
346+
// Discard modes, but the extracted commit is not None for InsertBefore:
347+
// it's just handled in a different way
287348
let extracted_commit_oid = match split_mode {
288-
SplitMode::Discard => None,
349+
SplitMode::InsertBefore | SplitMode::Discard => None,
289350
SplitMode::InsertAfter | SplitMode::DetachAfter => {
290351
let extracted_tree = repo.cherry_pick_fast(
291352
&target_commit,
@@ -300,7 +361,11 @@ pub fn split(
300361
&target_commit.get_committer(),
301362
format!("temp(split): {message}").as_str(),
302363
&extracted_tree,
303-
vec![&remainder_commit],
364+
if let SplitMode::InsertBefore = &split_mode {
365+
parent_commits.iter().collect()
366+
} else {
367+
vec![&remainder_commit]
368+
},
304369
)?;
305370

306371
// see git-branchless/src/commands/amend.rs:172
@@ -360,45 +425,73 @@ pub fn split(
360425
struct CleanUp {
361426
checkout_target: Option<CheckoutTarget>,
362427
rewritten_oids: Vec<(NonZeroOid, MaybeZeroOid)>,
428+
rebase_force_detach: bool,
363429
reset_index: bool,
364430
}
365431

366432
let cleanup = match (target_state, &split_mode, extracted_commit_oid) {
367-
// branch @ split commit checked out: extend branch to include extracted
368-
// 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
369436
(TargetState::CurrentBranch, SplitMode::InsertAfter, Some(extracted_commit_oid)) => {
370437
CleanUp {
371438
checkout_target: None,
372439
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(extracted_commit_oid))],
440+
rebase_force_detach: false,
373441
reset_index: false,
374442
}
375443
}
376444

445+
// same as above, but Discard; don't move branches, but do force reset
377446
(TargetState::CurrentBranch, SplitMode::Discard, None) => CleanUp {
378447
checkout_target: None,
379448
rewritten_oids: vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
449+
rebase_force_detach: false,
380450
reset_index: true,
381451
},
382452

383-
// commit to split checked out as detached HEAD, don't extend any
384-
// branches, but explicitly check out the newly split commit
385-
(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 {
386468
checkout_target: Some(CheckoutTarget::Oid(remainder_commit_oid)),
387469
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,
388479
reset_index: false,
389480
},
390481

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

399491
let CleanUp {
400492
checkout_target,
401493
rewritten_oids,
494+
rebase_force_detach,
402495
reset_index,
403496
} = cleanup;
404497

@@ -444,21 +537,18 @@ pub fn split(
444537
}
445538

446539
let mut builder = RebasePlanBuilder::new(&dag, permissions);
447-
let children = dag.query_children(CommitSet::from(target_oid))?;
448-
for child in dag.commit_set_to_vec(&children)? {
449-
match (&split_mode, extracted_commit_oid) {
450-
(_, None) => builder.move_subtree(child, vec![remainder_commit_oid])?,
451-
(_, Some(extracted_commit_oid)) => {
452-
builder.move_subtree(child, vec![extracted_commit_oid])?
453-
}
454-
}
455-
456-
match (&split_mode, extracted_commit_oid) {
457-
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
458-
builder.move_subtree(child, vec![remainder_commit_oid])?
459-
}
460-
(_, Some(extracted_commit_oid)) => {
461-
builder.move_subtree(child, vec![extracted_commit_oid])?
540+
if let SplitMode::InsertBefore = &split_mode {
541+
builder.move_subtree(target_oid, vec![remainder_commit_oid])?
542+
} else {
543+
let children = dag.query_children(CommitSet::from(target_oid))?;
544+
for child in dag.commit_set_to_vec(&children)? {
545+
match (&split_mode, extracted_commit_oid) {
546+
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
547+
builder.move_subtree(child, vec![remainder_commit_oid])?
548+
}
549+
(_, Some(extracted_commit_oid)) => {
550+
builder.move_subtree(child, vec![extracted_commit_oid])?
551+
}
462552
}
463553
}
464554
}
@@ -479,7 +569,7 @@ pub fn split(
479569
resolve_merge_conflicts,
480570
check_out_commit_options: CheckOutCommitOptions {
481571
additional_args: Default::default(),
482-
force_detach: false,
572+
force_detach: rebase_force_detach,
483573
reset: false,
484574
render_smartlog: false,
485575
},

0 commit comments

Comments
 (0)