Skip to content

Commit 6712829

Browse files
feat(split): add --before flag to insert extracted changes before split commit
1 parent a1fb2bf commit 6712829

File tree

4 files changed

+502
-53
lines changed

4 files changed

+502
-53
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: 136 additions & 48 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(),
@@ -209,6 +239,15 @@ pub fn split(
209239

210240
let target_entry = target_tree.get_path(path)?;
211241
let temp_tree_oid = match (parent_entry, target_entry, &split_mode) {
242+
// added/modified & InsertBefore => add to extracted commit
243+
(None, Some(commit_entry), SplitMode::InsertBefore)
244+
| (Some(_), Some(commit_entry), SplitMode::InsertBefore) => {
245+
remainder_tree.add_or_replace(&repo, path, &commit_entry)?
246+
}
247+
248+
// removed & InsertBefore => remove from remainder commit
249+
(Some(_), None, SplitMode::InsertBefore) => remainder_tree.remove(&repo, path)?,
250+
212251
// added => remove from remainder commit
213252
(None, Some(_), SplitMode::InsertAfter)
214253
| (None, Some(_), SplitMode::DetachAfter)
@@ -242,7 +281,11 @@ pub fn split(
242281
let message = {
243282
let (effects, _progress) =
244283
effects.start_operation(lib::core::effects::OperationType::CalculateDiff);
245-
let (old_tree, new_tree) = (&remainder_tree, &target_tree);
284+
let (old_tree, new_tree) = if let SplitMode::InsertBefore = &split_mode {
285+
(&parent_tree, &remainder_tree)
286+
} else {
287+
(&remainder_tree, &target_tree)
288+
};
246289
let diff = repo.get_diff_between_trees(
247290
&effects,
248291
Some(old_tree),
@@ -253,8 +296,23 @@ pub fn split(
253296
summarize_diff_for_temporary_commit(&diff)?
254297
};
255298

256-
let remainder_commit_oid =
257-
target_commit.amend_commit(None, None, None, None, Some(&remainder_tree))?;
299+
// before => split commit is created on parent as "extracted", target is rebased onto split
300+
// after => target is amended as "split", split is cherry picked onto split as "extracted"
301+
302+
// FIXME terminology is wrong here: remainder is correct for "After", but
303+
// this is the "extracted" commit for InsertBefore
304+
let remainder_commit_oid = if let SplitMode::InsertBefore = split_mode {
305+
repo.create_commit(
306+
None,
307+
&target_commit.get_author(),
308+
&target_commit.get_committer(),
309+
format!("temp(split): {message}").as_str(),
310+
&remainder_tree,
311+
parent_commits.iter().collect(),
312+
)?
313+
} else {
314+
target_commit.amend_commit(None, None, None, None, Some(&remainder_tree))?
315+
};
258316
let remainder_commit = repo.find_commit_or_fail(remainder_commit_oid)?;
259317

260318
if remainder_commit.is_empty() {
@@ -273,8 +331,11 @@ pub fn split(
273331
new_commit_oid: MaybeZeroOid::NonZero(remainder_commit_oid),
274332
}])?;
275333

334+
// FIXME terminology is wrong here: extracted is correct for After and
335+
// Discard modes, but the extracted commit is not None for InsertBefore:
336+
// it's just handled in a different way
276337
let extracted_commit_oid = match split_mode {
277-
SplitMode::Discard => None,
338+
SplitMode::InsertBefore | SplitMode::Discard => None,
278339
SplitMode::InsertAfter | SplitMode::DetachAfter => {
279340
let extracted_tree = repo.cherry_pick_fast(
280341
&target_commit,
@@ -289,7 +350,11 @@ pub fn split(
289350
&target_commit.get_committer(),
290351
format!("temp(split): {message}").as_str(),
291352
&extracted_tree,
292-
vec![&remainder_commit],
353+
if let SplitMode::InsertBefore = &split_mode {
354+
parent_commits.iter().collect()
355+
} else {
356+
vec![&remainder_commit]
357+
},
293358
)?;
294359

295360
// see git-branchless/src/commands/amend.rs:172
@@ -321,42 +386,68 @@ pub fn split(
321386
)?;
322387

323388
let head_info = repo.get_head_info()?;
324-
let (checkout_target, rewritten_oids) = match head_info {
325-
// branch @ split commit checked out: extend branch to include extracted
326-
// commit; branch will stay checked out w/o any explicit checkout
327-
ResolvedReferenceInfo {
328-
oid: Some(oid),
329-
reference_name: Some(_),
330-
} if oid == target_oid
331-
&& split_mode != SplitMode::DetachAfter
332-
&& extracted_commit_oid.is_some() =>
333-
{
334-
(
335-
None,
336-
vec![(
337-
target_oid,
338-
MaybeZeroOid::NonZero(extracted_commit_oid.unwrap()),
339-
)],
340-
)
341-
}
389+
let (checkout_target, rewritten_oids, rebase_force_detach) = match (head_info, &split_mode) {
390+
// branch @ target commit checked out: extend branch to include extracted
391+
// commit; branch will stay checked out w/o any explicit checkout ResolvedReferenceInfo {
392+
(
393+
ResolvedReferenceInfo {
394+
oid: Some(oid),
395+
reference_name: Some(_),
396+
},
397+
// not DetatchAfter
398+
SplitMode::InsertAfter | SplitMode::Discard,
399+
) if oid == target_oid && extracted_commit_oid.is_some() => (
400+
None,
401+
vec![(
402+
target_oid,
403+
MaybeZeroOid::NonZero(extracted_commit_oid.unwrap()),
404+
)],
405+
false,
406+
),
407+
408+
// same as above, but InsertBefore; do not move branches
409+
(
410+
ResolvedReferenceInfo {
411+
oid: Some(oid),
412+
reference_name: Some(_),
413+
},
414+
SplitMode::InsertBefore,
415+
) if oid == target_oid => (None, vec![], false),
342416

343-
// commit to split checked out as detached HEAD, don't extend any
417+
// target checked out as detached HEAD, don't extend any
344418
// branches, but explicitly check out the newly split commit
345-
ResolvedReferenceInfo {
346-
oid: Some(oid),
347-
reference_name: None,
348-
} if oid == target_oid => (
419+
(
420+
ResolvedReferenceInfo {
421+
oid: Some(oid),
422+
reference_name: None,
423+
},
424+
SplitMode::InsertAfter | SplitMode::Discard | SplitMode::DetachAfter,
425+
) if oid == target_oid => (
349426
Some(CheckoutTarget::Oid(remainder_commit_oid)),
350427
vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
428+
false,
351429
),
352430

431+
// same as above, but InsertBefore; do not move branches
432+
(
433+
ResolvedReferenceInfo {
434+
oid: Some(oid),
435+
reference_name: None,
436+
},
437+
SplitMode::InsertBefore,
438+
) if oid == target_oid => (None, vec![], true),
439+
353440
// some other commit or branch was checked out, default behavior is fine
354-
ResolvedReferenceInfo {
355-
oid: _,
356-
reference_name: _,
357-
} => (
441+
(
442+
ResolvedReferenceInfo {
443+
oid: _,
444+
reference_name: _,
445+
},
446+
_,
447+
) => (
358448
None,
359449
vec![(target_oid, MaybeZeroOid::NonZero(remainder_commit_oid))],
450+
false,
360451
),
361452
};
362453

@@ -386,21 +477,18 @@ pub fn split(
386477
}
387478

388479
let mut builder = RebasePlanBuilder::new(&dag, permissions);
389-
let children = dag.query_children(CommitSet::from(target_oid))?;
390-
for child in dag.commit_set_to_vec(&children)? {
391-
match (&split_mode, extracted_commit_oid) {
392-
(_, None) => builder.move_subtree(child, vec![remainder_commit_oid])?,
393-
(_, Some(extracted_commit_oid)) => {
394-
builder.move_subtree(child, vec![extracted_commit_oid])?
395-
}
396-
}
397-
398-
match (&split_mode, extracted_commit_oid) {
399-
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
400-
builder.move_subtree(child, vec![remainder_commit_oid])?
401-
}
402-
(_, Some(extracted_commit_oid)) => {
403-
builder.move_subtree(child, vec![extracted_commit_oid])?
480+
if let SplitMode::InsertBefore = &split_mode {
481+
builder.move_subtree(target_oid, vec![remainder_commit_oid])?
482+
} else {
483+
let children = dag.query_children(CommitSet::from(target_oid))?;
484+
for child in dag.commit_set_to_vec(&children)? {
485+
match (&split_mode, extracted_commit_oid) {
486+
(_, None) | (SplitMode::DetachAfter, Some(_)) => {
487+
builder.move_subtree(child, vec![remainder_commit_oid])?
488+
}
489+
(_, Some(extracted_commit_oid)) => {
490+
builder.move_subtree(child, vec![extracted_commit_oid])?
491+
}
404492
}
405493
}
406494
}
@@ -421,7 +509,7 @@ pub fn split(
421509
resolve_merge_conflicts,
422510
check_out_commit_options: CheckOutCommitOptions {
423511
additional_args: Default::default(),
424-
force_detach: false,
512+
force_detach: rebase_force_detach,
425513
reset: false,
426514
render_smartlog: false,
427515
},

0 commit comments

Comments
 (0)