diff --git a/CHANGELOG.md b/CHANGELOG.md index a40248ecd..45f2bdf8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - (#1464): created `git split` command to extract changes from a commit - (#1603): added `git move --dry-run` to test in-memory rebases - (#1604): `git record` and `git amend` can now automatically detect and begin tracking new files (optional, disabled by default) +- (#1612): added `git record --new` to create new, empty commits - (#1632): added `git record --fixup` option, to create a fixup commit (similar to `reword --fixup`) ### Changed diff --git a/git-branchless-lib/src/git/mod.rs b/git-branchless-lib/src/git/mod.rs index 60e044094..2d9e81a73 100644 --- a/git-branchless-lib/src/git/mod.rs +++ b/git-branchless-lib/src/git/mod.rs @@ -23,8 +23,8 @@ pub use reference::{ }; pub use repo::{ AmendFastOptions, CherryPickFastOptions, CreateCommitFastError, Error as RepoError, - GitErrorCode, GitVersion, PatchId, Repo, ResolvedReferenceInfo, Result as RepoResult, Time, - message_prettify, + GitErrorCode, GitVersion, PatchId, Repo, ResolvedReferenceInfo, Result as RepoResult, + Signature, Time, message_prettify, }; pub use run::{GitRunInfo, GitRunOpts, GitRunResult}; pub use snapshot::{WorkingCopyChangesType, WorkingCopySnapshot}; diff --git a/git-branchless-lib/src/git/repo.rs b/git-branchless-lib/src/git/repo.rs index 9aa7d53a0..b729026a2 100644 --- a/git-branchless-lib/src/git/repo.rs +++ b/git-branchless-lib/src/git/repo.rs @@ -1659,6 +1659,18 @@ impl std::fmt::Debug for Signature<'_> { } impl<'repo> Signature<'repo> { + /// Create a new signature. + #[instrument] + pub fn new(name: &str, email: &str, now: &SystemTime) -> Result { + Ok({ + Signature { + inner: git2::Signature::now(name, email).map_err(Error::CreateSignature)?, + } + .update_timestamp(*now)? + }) + } + + /// Create an automated signature, for internal use. #[instrument] pub fn automated() -> Result { Ok(Signature { @@ -1708,10 +1720,12 @@ impl<'repo> Signature<'repo> { } } + /// Get the name applied to this signature. pub fn get_name(&self) -> Option<&str> { self.inner.name() } + /// Get the email applied to this signature. pub fn get_email(&self) -> Option<&str> { self.inner.email() } diff --git a/git-branchless-opts/src/lib.rs b/git-branchless-opts/src/lib.rs index 32c317a98..32f0dd8ae 100644 --- a/git-branchless-opts/src/lib.rs +++ b/git-branchless-opts/src/lib.rs @@ -344,6 +344,10 @@ pub struct RecordArgs { #[clap(action, short = 's', long = "stash", conflicts_with_all(&["create", "detach"]))] pub stash: bool, + /// Create an empty commit, leaving any changes uncommitted. + #[clap(action, long = "new", conflicts_with("stash"))] + pub new: bool, + /// How should newly encountered, untracked files be handled? #[clap(value_parser, long = "untracked", conflicts_with_all(&["interactive"]))] pub untracked_file_strategy: Option, diff --git a/git-branchless-record/src/lib.rs b/git-branchless-record/src/lib.rs index 766fc6313..40d325e52 100644 --- a/git-branchless-record/src/lib.rs +++ b/git-branchless-record/src/lib.rs @@ -14,6 +14,7 @@ use std::ffi::OsString; use std::fmt::Write; use std::time::SystemTime; +use eyre::OptionExt; use git_branchless_invoke::CommandContext; use git_branchless_opts::{MessageArgs, RecordArgs, ResolveRevsetOptions, Revset}; use git_branchless_reword::{ResolveFixupCommitError, edit_message, resolve_commit_to_fixup}; @@ -32,9 +33,10 @@ use lib::core::rewrite::{ }; use lib::core::untracked_file_cache::{UntrackedFileStrategy, process_untracked_files}; use lib::git::{ - CategorizedReferenceName, FileMode, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo, - ResolvedReferenceInfo, Stage, UpdateIndexCommand, WorkingCopyChangesType, WorkingCopySnapshot, - process_diff_for_record, summarize_diff_for_temporary_commit, update_index, + CategorizedReferenceName, ConfigRead, FileMode, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo, + ResolvedReferenceInfo, Signature, Stage, UpdateIndexCommand, WorkingCopyChangesType, + WorkingCopySnapshot, process_diff_for_record, summarize_diff_for_temporary_commit, + update_index, }; use lib::try_exit_code; use lib::util::{ExitCode, EyreExitOr}; @@ -62,6 +64,7 @@ pub fn command_main(ctx: CommandContext, args: RecordArgs) -> EyreExitOr<()> { detach, insert, stash, + new, untracked_file_strategy, } = args; record( @@ -74,6 +77,7 @@ pub fn command_main(ctx: CommandContext, args: RecordArgs) -> EyreExitOr<()> { detach, insert, stash, + new, untracked_file_strategy, ) } @@ -89,6 +93,7 @@ fn record( detach: bool, insert: bool, stash: bool, + new: bool, untracked_file_strategy: Option, ) -> EyreExitOr<()> { let now = SystemTime::now(); @@ -97,11 +102,164 @@ fn record( let event_log_db = EventLogDb::new(&conn)?; let event_tx_id = event_log_db.make_transaction_id(now, "record")?; + if new { + try_exit_code!(create_new_commit( + &repo, + &now, + git_run_info, + effects, + &event_log_db, + &event_tx_id, + messages, + )?); + } else { + try_exit_code!(create_commit_with_changes( + &repo, + git_run_info, + effects, + &event_log_db, + &event_tx_id, + messages, + commit_to_fixup, + interactive, + branch_name, + stash, + untracked_file_strategy + )?); + } + + if detach || stash { + let head_info = repo.get_head_info()?; + let checkout_target = match &head_info { + ResolvedReferenceInfo { + oid: None, + reference_name: Some(reference_name), + } => { + // FIXME: unborn HEAD, what to do? + Some(CheckoutTarget::Reference(reference_name.clone())) + } + + ResolvedReferenceInfo { + oid: Some(oid), + reference_name: Some(reference_name), + } => { + let head_commit = repo.find_commit_or_fail(*oid)?; + match head_commit.get_parents().as_slice() { + [] => try_exit_code!(git_run_info.run( + effects, + Some(event_tx_id), + &[ + "update-ref", + "-d", + reference_name.as_str(), + &oid.to_string(), + ], + )?), + [parent_commit] => { + let branch_name = + CategorizedReferenceName::new(reference_name).render_suffix(); + repo.detach_head(&head_info)?; + try_exit_code!(git_run_info.run( + effects, + Some(event_tx_id), + &[ + "branch", + "-f", + &branch_name, + &parent_commit.get_oid().to_string(), + ], + )?); + } + parent_commits => { + eyre::bail!( + "git-branchless record --detach called on a merge commit, but it should only be capable of creating zero- or one-parent commits. Parents: {parent_commits:?}" + ); + } + } + + Some(CheckoutTarget::Reference(reference_name.clone())) + } + + ResolvedReferenceInfo { + oid: Some(oid), + reference_name: None, + } => { + let head_commit = repo.find_commit_or_fail(*oid)?; + match head_commit.get_parents().as_slice() { + [] => { + eyre::bail!( + "git-branchless record --stash seems to have created a root commit (commit without parents), but this should be impossible." + ); + } + [parent_commit] => Some(CheckoutTarget::Oid(parent_commit.get_oid())), + parent_commits => { + eyre::bail!( + "git-branchless record --stash seems to have created a merge commit, but this should be impossible. Parents: {parent_commits:?}" + ); + } + } + } + + ResolvedReferenceInfo { + oid: None, + reference_name: None, + } => None, + }; + + if stash && checkout_target.is_some() { + try_exit_code!(check_out_commit( + effects, + git_run_info, + &repo, + &event_log_db, + event_tx_id, + checkout_target, + &CheckOutCommitOptions { + additional_args: vec![], + force_detach: false, + reset: false, + render_smartlog: false, + }, + )?); + } + } + + if insert { + try_exit_code!(insert_before_siblings( + effects, + git_run_info, + now, + event_tx_id + )?); + } + + Ok(Ok(())) +} + +#[instrument] +fn create_commit_with_changes( + repo: &Repo, + git_run_info: &GitRunInfo, + effects: &Effects, + event_log_db: &EventLogDb, + event_tx_id: &EventTransactionId, + messages: Vec, + commit_to_fixup: Option, + interactive: bool, + branch_name: Option, + stash: bool, + untracked_file_strategy: Option, +) -> EyreExitOr<()> { let (snapshot, working_copy_changes_type, files_to_add) = { let head_info = repo.get_head_info()?; let index = repo.get_index()?; - let (snapshot, _status) = - repo.get_status(effects, git_run_info, &index, &head_info, Some(event_tx_id))?; + let (snapshot, _status) = repo.get_status( + effects, + git_run_info, + &index, + &head_info, + Some(*event_tx_id), + )?; let working_copy_changes_type = snapshot.get_working_copy_changes_type()?; let files_to_add = match working_copy_changes_type { @@ -112,8 +270,8 @@ fn record( try_exit_code!(process_untracked_files( effects, git_run_info, - &repo, - event_tx_id, + repo, + *event_tx_id, untracked_file_strategy, )?) }; @@ -136,8 +294,8 @@ fn record( try_exit_code!(process_untracked_files( effects, git_run_info, - &repo, - event_tx_id, + repo, + *event_tx_id, untracked_file_strategy, )?) } @@ -161,9 +319,9 @@ fn record( try_exit_code!(check_out_commit( effects, git_run_info, - &repo, - &event_log_db, - event_tx_id, + repo, + event_log_db, + *event_tx_id, None, &CheckOutCommitOptions { additional_args: vec![OsString::from("-b"), OsString::from(branch_name)], @@ -184,16 +342,16 @@ fn record( effects.get_output_stream(), "Either commit or unstage your changes and try again. Aborting." )?; - return Ok(Err(ExitCode(1))); + Ok(Err(ExitCode(1))) } else { - try_exit_code!(record_interactive( + record_interactive( effects, git_run_info, - &repo, + repo, &snapshot, - event_tx_id, + *event_tx_id, messages, - )?); + ) } } else { if !files_to_add.is_empty() { @@ -218,11 +376,11 @@ fn record( args.extend(files_to_add.iter().map(|p| format!(":/{p}"))); args }; - let _ = git_run_info.run_direct_no_wrapping(Some(event_tx_id), &args)?; + let _ = git_run_info.run_direct_no_wrapping(Some(*event_tx_id), &args)?; } let messages = if messages.is_empty() && stash { - get_default_stash_message(&repo, effects, &snapshot, &working_copy_changes_type) + get_default_stash_message(repo, effects, &snapshot, &working_copy_changes_type) .map(|message| vec![message])? } else { messages @@ -238,13 +396,12 @@ fn record( args.push("--all".to_string()); } if let Some(revset) = commit_to_fixup { - let event_replayer = - EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?; + let event_replayer = EventReplayer::from_event_log_db(effects, repo, event_log_db)?; let event_cursor = event_replayer.make_default_cursor(); let references_snapshot = repo.get_references_snapshot()?; let mut dag = Dag::open_and_sync( effects, - &repo, + repo, &event_replayer, event_cursor, &references_snapshot, @@ -252,7 +409,7 @@ fn record( let head: Option<&[lib::git::Commit<'_>]> = snapshot.head_commit.as_ref().map(std::slice::from_ref); let commit = match resolve_commit_to_fixup( - &repo, + repo, &mut dag, effects, &revset, @@ -283,113 +440,91 @@ fn record( }; args }; - try_exit_code!(git_run_info.run_direct_no_wrapping(Some(event_tx_id), &args)?); + git_run_info.run_direct_no_wrapping(Some(*event_tx_id), &args) } +} - if detach || stash { - let head_info = repo.get_head_info()?; - let checkout_target = match &head_info { - ResolvedReferenceInfo { - oid: None, - reference_name: Some(reference_name), - } => { - // FIXME: unborn HEAD, what to do? - Some(CheckoutTarget::Reference(reference_name.clone())) - } - - ResolvedReferenceInfo { - oid: Some(oid), - reference_name: Some(reference_name), - } => { - let head_commit = repo.find_commit_or_fail(*oid)?; - match head_commit.get_parents().as_slice() { - [] => try_exit_code!(git_run_info.run( - effects, - Some(event_tx_id), - &[ - "update-ref", - "-d", - reference_name.as_str(), - &oid.to_string(), - ], - )?), - [parent_commit] => { - let branch_name = - CategorizedReferenceName::new(reference_name).render_suffix(); - repo.detach_head(&head_info)?; - try_exit_code!(git_run_info.run( - effects, - Some(event_tx_id), - &[ - "branch", - "-f", - &branch_name, - &parent_commit.get_oid().to_string(), - ], - )?); - } - parent_commits => { - eyre::bail!( - "git-branchless record --detach called on a merge commit, but it should only be capable of creating zero- or one-parent commits. Parents: {parent_commits:?}" - ); - } - } +#[instrument] +fn create_new_commit( + repo: &Repo, + now: &SystemTime, + git_run_info: &GitRunInfo, + effects: &Effects, + event_log_db: &EventLogDb, + event_tx_id: &EventTransactionId, + messages: Vec, +) -> EyreExitOr<()> { + let head_info = repo.get_head_info()?; + let current_commit_oid = match head_info { + ResolvedReferenceInfo { + oid: Some(oid), + reference_name: _, + } => oid, + ResolvedReferenceInfo { + oid: None, + reference_name: _, + } => unimplemented!("unborn head"), + }; - Some(CheckoutTarget::Reference(reference_name.clone())) - } + let current_commit = repo.find_commit_or_fail(current_commit_oid)?; + let current_tree = current_commit.get_tree()?; + + let (author, committer) = { + let config = repo.get_readonly_config()?; + let name: String = config + .get("user.name")? + .ok_or_eyre("TODO no user.name configured")?; + let email: String = config + .get("user.email")? + .ok_or_eyre("TODO no user.email configured")?; + + // TODO: support GIT_AUTHOR_DATE/GIT_COMMITTER_DATE + // FIXME: is there a better way to mock the time? `git` will use the + // `GIT_AUTHOR_DATE` env, but we don't (as far as I can tell) create + // entirely new commits like this elsewhere + let (author_date, committer_date) = + if std::env::var("TEST_RECORD_NEW_FAKE_COMMIT_TIME").is_ok() { + ( + current_commit.get_author().get_time().to_system_time()?, + current_commit.get_committer().get_time().to_system_time()?, + ) + } else { + (*now, *now) + }; - ResolvedReferenceInfo { - oid: Some(oid), - reference_name: None, - } => { - let head_commit = repo.find_commit_or_fail(*oid)?; - match head_commit.get_parents().as_slice() { - [] => { - eyre::bail!( - "git-branchless record --stash seems to have created a root commit (commit without parents), but this should be impossible." - ); - } - [parent_commit] => Some(CheckoutTarget::Oid(parent_commit.get_oid())), - parent_commits => { - eyre::bail!( - "git-branchless record --stash seems to have created a merge commit, but this should be impossible. Parents: {parent_commits:?}" - ); - } - } - } + ( + Signature::new(&name, &email, &author_date)?, + Signature::new(&name, &email, &committer_date)?, + ) + }; - ResolvedReferenceInfo { - oid: None, - reference_name: None, - } => None, - }; + let new_oid = repo.create_commit( + None, + &author, + &committer, + &messages.join("\n\n"), + ¤t_tree, + vec![¤t_commit], + )?; - if stash && checkout_target.is_some() { - try_exit_code!(check_out_commit( - effects, - git_run_info, - &repo, - &event_log_db, - event_tx_id, - checkout_target, - &CheckOutCommitOptions { - additional_args: vec![], - force_detach: false, - reset: false, - render_smartlog: false, - }, - )?); - } - } + // TODO: move branch if ref_name + // TODO: mark commit reachable (will show in sl if descendants, but not if detached) + // FIXME: --new --create doesn't seem to work - if insert { - try_exit_code!(insert_before_siblings( - effects, - git_run_info, - now, - event_tx_id - )?); - } + try_exit_code!(check_out_commit( + effects, + git_run_info, + repo, + event_log_db, + *event_tx_id, + Some(CheckoutTarget::Oid(new_oid)), + &CheckOutCommitOptions { + additional_args: vec![], + force_detach: false, + reset: false, + render_smartlog: false, + }, + )?); Ok(Ok(())) } diff --git a/git-branchless-record/tests/test_record.rs b/git-branchless-record/tests/test_record.rs index 941f8ae92..753eeb065 100644 --- a/git-branchless-record/tests/test_record.rs +++ b/git-branchless-record/tests/test_record.rs @@ -503,6 +503,186 @@ fn test_record_staged_changes_interactive() -> eyre::Result<()> { Ok(()) } +#[test] +fn test_record_new() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_reference_transactions()? { + return Ok(()); + } + git.init_repo()?; + + git.commit_file("test1", 1)?; + + { + // --new w/ changes in the working copy + git.write_file_txt("test1", "new test1 contents\n")?; + + let (stdout, _stderr) = git.branchless("record", &["-m", "empty commit 1", "--new"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout ebf97de456b71d33b95e6fd0a28139ece0f209d0 -- + M test1.txt + "###); + + let (stdout, _stderr) = git.run(&["show"])?; + insta::assert_snapshot!(stdout, @r###" + commit ebf97de456b71d33b95e6fd0a28139ece0f209d0 + Author: Testy McTestface + Date: Thu Oct 29 12:34:56 2020 -0100 + + empty commit 1 + "###); + + let stdout = git.smartlog()?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + @ ebf97de empty commit 1 + "###); + } + + { + // --new w/ changes in the index + git.run(&["add", "test1.txt"])?; + + let (stdout, _stderr) = git.branchless("record", &["-m", "empty commit 2", "--new"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 064c1bc41faa18fbe1332a5190976112f7b89fdb -- + M test1.txt + "###); + + let (stdout, _stderr) = git.run(&["show"])?; + insta::assert_snapshot!(stdout, @r###" + commit 064c1bc41faa18fbe1332a5190976112f7b89fdb + Author: Testy McTestface + Date: Thu Oct 29 12:34:56 2020 -0100 + + empty commit 2 + "###); + + let stdout = git.smartlog()?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o ebf97de empty commit 1 + | + @ 064c1bc empty commit 2 + "###); + } + + Ok(()) +} + +#[test] +fn test_record_new_uses_user_name_and_email() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_reference_transactions()? { + return Ok(()); + } + git.init_repo()?; + git.commit_file("test1", 1)?; + + { + let (stdout, _stderr) = git.run(&["show", "--name-only"])?; + insta::assert_snapshot!(stdout, @r###" + commit 62fc20d2a290daea0d52bdc2ed2ad4be6491010e + Author: Testy McTestface + Date: Thu Oct 29 12:34:56 2020 -0100 + + create test1.txt + + test1.txt + "###); + } + + { + git.run(&["config", "user.name", "Uncreative User Name"])?; + git.run(&["config", "user.email", "boring@example.com"])?; + git.write_file_txt("test1", "new test1 contents\n")?; + + let (stdout, _stderr) = git.branchless_with_options( + "record", + &["-m", "empty commit 1", "--new"], + &GitRunOptions { + time: 2, + env: { + [("TEST_RECORD_NEW_FAKE_COMMIT_TIME", "true")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }, + ..Default::default() + }, + )?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 30fa5a52eb8542e52686e6cd34bb40fdb99ba09f -- + M test1.txt + "###); + + let (stdout, _stderr) = git.run(&["show"])?; + insta::assert_snapshot!(_stderr+&stdout, @r###" + commit 30fa5a52eb8542e52686e6cd34bb40fdb99ba09f + Author: Uncreative User Name + Date: Thu Oct 29 08:34:56 2020 -0500 + + empty commit 1 + "###); + } + + Ok(()) +} + +#[test] +fn test_record_new_insert() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_reference_transactions()? { + return Ok(()); + } + git.init_repo()?; + + git.run(&["switch", "--create", "test"])?; + git.commit_file("test1", 1)?; + git.commit_file("test2", 1)?; + git.run(&["checkout", "HEAD^"])?; + git.write_file_txt("test1", "new test1 contents\n")?; + + let (stdout, _stderr) = + git.branchless("record", &["-m", "empty commit", "--new", "--insert"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 910c8aacfa6e8f315cd578772e7bab2e636dacb5 -- + M test1.txt + Attempting rebase in-memory... + [1/1] Committed as: ae12a93 create test2.txt + branchless: processing 1 update: branch test + branchless: processing 1 rewritten commit + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show"])?; + insta::assert_snapshot!(stdout, @r###" + commit 910c8aacfa6e8f315cd578772e7bab2e636dacb5 + Author: Testy McTestface + Date: Thu Oct 29 12:34:56 2020 -0100 + + empty commit + "###); + + let stdout = git.smartlog()?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ 910c8aa empty commit + | + o ae12a93 (test) create test2.txt + "###); + + Ok(()) +} + #[test] fn test_record_detach() -> eyre::Result<()> { let git = make_git()?; diff --git a/git-branchless/src/commands/split.rs b/git-branchless/src/commands/split.rs index 02f3b3748..31a11d245 100644 --- a/git-branchless/src/commands/split.rs +++ b/git-branchless/src/commands/split.rs @@ -530,6 +530,7 @@ pub fn split( builder.move_subtree(target_oid, vec![remainder_commit_oid])? } else { let children = dag.query_children(CommitSet::from(target_oid))?; + let children = dag.filter_visible_commits(children)?; for child in dag.commit_set_to_vec(&children)? { match (&split_mode, extracted_commit_oid) { (_, None) | (SplitMode::DetachAfter, Some(_)) => { diff --git a/git-branchless/tests/test_split.rs b/git-branchless/tests/test_split.rs index 04f3e6318..14008ef02 100644 --- a/git-branchless/tests/test_split.rs +++ b/git-branchless/tests/test_split.rs @@ -1394,3 +1394,79 @@ fn test_split_will_not_split_to_empty_commit() -> eyre::Result<()> { Ok(()) } + +#[test] +fn test_split_does_not_unhide_obsolete_children() -> eyre::Result<()> { + let git = make_git()?; + git.init_repo()?; + git.detach_head()?; + + { + // Create test1.txt and test2.txt in a single commit + git.write_file_txt("test1", "contents1")?; + git.write_file_txt("test2", "contents2")?; + git.run(&["add", "."])?; + git.run(&["commit", "-m", "first commit"])?; + + // Create a third file in "second commit" + git.write_file_txt("test3", "contents3")?; + git.run(&["add", "."])?; + git.run(&["commit", "-m", "second commit"])?; + + // verify initial state + let (stdout, _stderr) = git.branchless("smartlog", &[])?; + insta::assert_snapshot!(&stdout, @r###" + O f777ecc (master) create initial.txt + | + o 4d11d02 first commit + | + @ 61d094b second commit + "###); + } + + { + // amend the current commit in some way + git.write_file_txt("test2", "updated2")?; + git.branchless("amend", &[])?; + + let (stdout, _stderr) = git.branchless("smartlog", &[])?; + insta::assert_snapshot!(&stdout, @r###" + O f777ecc (master) create initial.txt + | + o 4d11d02 first commit + | + @ 6698c7b second commit + "###); + } + + { + // Split the first commit (HEAD~) by extracting test2.txt + // This will rebase children onto the split result. + // There should be only 1 child. split should not unhide previously + // hidden/rewritten/obsolete commits. + let (stdout, _stderr) = git.branchless("split", &["HEAD~", "test2.txt"])?; + insta::assert_snapshot!(&stdout, @r###" + Attempting rebase in-memory... + [1/1] Committed as: ae95d4c second commit + branchless: processing 1 rewritten commit + branchless: running command: checkout ae95d4c54730973107527df675e53de5aec4f855 -- + In-memory rebase succeeded. + O f777ecc (master) create initial.txt + | + o 8e5c74b first commit + | + o a55d783 temp(split): test2.txt (+1) + | + @ ae95d4c second commit + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--stat", "HEAD"])?; + insta::assert_snapshot!(&stdout, @" + test2.txt | 2 +- + test3.txt | 1 + + 2 files changed, 2 insertions(+), 1 deletion(-) + "); + } + + Ok(()) +}