From b163b8bbf7f0680636c0f687a47c3bc483f2a824 Mon Sep 17 00:00:00 2001 From: Clayton Carter Date: Tue, 20 Sep 2022 22:39:00 -0400 Subject: [PATCH] feat(move): Add `--fixup` to squash moved commits into the destination --- git-branchless/src/commands/mod.rs | 2 + git-branchless/src/commands/move.rs | 25 +- git-branchless/src/opts.rs | 4 + git-branchless/tests/command/test_move.rs | 721 ++++++++++++++++++++++ 4 files changed, 749 insertions(+), 3 deletions(-) diff --git a/git-branchless/src/commands/mod.rs b/git-branchless/src/commands/mod.rs index a7b66e553..871ca92f1 100644 --- a/git-branchless/src/commands/mod.rs +++ b/git-branchless/src/commands/mod.rs @@ -211,6 +211,7 @@ fn do_main_and_drop_locals() -> eyre::Result { dest, base, exact, + fixup, insert, move_options, } => r#move::r#move( @@ -220,6 +221,7 @@ fn do_main_and_drop_locals() -> eyre::Result { dest, base, exact, + fixup, insert, &move_options, )?, diff --git a/git-branchless/src/commands/move.rs b/git-branchless/src/commands/move.rs index 75361d4d6..f05391462 100644 --- a/git-branchless/src/commands/move.rs +++ b/git-branchless/src/commands/move.rs @@ -64,6 +64,7 @@ pub fn r#move( dest: Option, bases: Vec, exacts: Vec, + fixup: bool, insert: bool, move_options: &MoveOptions, ) -> eyre::Result { @@ -256,7 +257,7 @@ pub fn r#move( let commits_to_move = commits_to_move.union(&union_all( &exact_components.values().cloned().collect::>(), )); - let commits_to_move = if insert { + let commits_to_move = if insert || fixup { commits_to_move.union(&dag.query().children(CommitSet::from(dest_oid))?) } else { commits_to_move @@ -275,7 +276,18 @@ pub fn r#move( let source_roots = dag.query().roots(source_oids.clone())?; for source_root in commit_set_to_vec_unsorted(&source_roots)? { - builder.move_subtree(source_root, vec![dest_oid])?; + if fixup { + let commits = dag + .query() + .descendants(CommitSet::from(source_root))? + .difference(&dag.obsolete_commits); + let commits = commit_set_to_vec_unsorted(&commits)?; + for commit in commits.iter() { + builder.fixup_commit(*commit, dest_oid)?; + } + } else { + builder.move_subtree(source_root, vec![dest_oid])?; + } } let component_roots: CommitSet = exact_components.keys().cloned().collect(); @@ -384,7 +396,14 @@ pub fn r#move( } } - builder.move_subtree(component_root, vec![component_dest_oid])?; + if fixup { + let commits = commit_set_to_vec_unsorted(component)?; + for commit in commits.iter() { + builder.fixup_commit(*commit, dest_oid)?; + } + } else { + builder.move_subtree(component_root, vec![component_dest_oid])?; + } } if insert { diff --git a/git-branchless/src/opts.rs b/git-branchless/src/opts.rs index dea444095..b0a588908 100644 --- a/git-branchless/src/opts.rs +++ b/git-branchless/src/opts.rs @@ -319,6 +319,10 @@ pub enum Command { #[clap(value_parser, short = 'd', long = "dest")] dest: Option, + /// Combine the moved commits and squash into the destination commit. + #[clap(action, short = 'F', long = "fixup", conflicts_with = "insert")] + fixup: bool, + /// Insert the subtree between the destination and it's children, if any. /// Only supported if the moved subtree has a single head. #[clap(action, short = 'I', long = "insert")] diff --git a/git-branchless/tests/command/test_move.rs b/git-branchless/tests/command/test_move.rs index 2f0bb615d..d58aa5156 100644 --- a/git-branchless/tests/command/test_move.rs +++ b/git-branchless/tests/command/test_move.rs @@ -4500,3 +4500,724 @@ fn test_move_public_commit() -> eyre::Result<()> { Ok(()) } + +#[test] +fn test_move_fixup_head_into_parent() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let _test1_oid = git.commit_file("test1", 1)?; + let test2_oid = git.commit_file("test2", 2)?; + let _test3_oid = git.commit_file("test3", 3)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 96d1c37 create test2.txt + | + @ 70deb1e create test3.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test3.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--fixup", + "-s", + &"HEAD".to_string(), + "-d", + &test2_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ 68aa706 create test2.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test3.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-s", + &"HEAD".to_string(), + "-d", + &test2_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/2] Committed as: 96d1c37 create test2.txt + [2/2] Amended as: 68aa706 create test2.txt + branchless: processing 2 rewritten commits + branchless: running command: checkout 68aa7060ef38274a33c9a51dc6964de6e84fbd43 + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ 68aa706 create test2.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test3.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_fixup_parent_into_head() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let _test1_oid = git.commit_file("test1", 1)?; + let test2_oid = git.commit_file("test2", 2)?; + let _test3_oid = git.commit_file("test3", 3)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 96d1c37 create test2.txt + | + @ 70deb1e create test3.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test3.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&["move", "--on-disk", "--fixup", "-x", &test2_oid.to_string()])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ 2565373 create test3.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test3.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-x", + &test2_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/2] Committed as: 4838e49 create test3.txt + [2/2] Amended as: 2565373 create test3.txt + branchless: processing 2 rewritten commits + branchless: running command: checkout 2565373efecda4dda6274ad5107dbff6be05f0a3 + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ 2565373 create test3.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test3.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_fixup_multiple_into_ancestor() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let test1_oid = git.commit_file("test1", 1)?; + let test2_oid = git.commit_file("test2", 2)?; + let _test3_oid = git.commit_file("test3", 3)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 96d1c37 create test2.txt + | + @ 70deb1e create test3.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test3.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--fixup", + "-x", + &format!("{}+{}", test1_oid, test2_oid), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + @ dbc9c34 create test3.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + test2.txt + test3.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-x", + &format!("{}+{}", test1_oid, test2_oid), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/3] Committed as: 98b9119 create test3.txt + [2/3] Amended as: 9c45567 create test3.txt + [3/3] Amended as: dbc9c34 create test3.txt + branchless: processing 3 rewritten commits + branchless: running command: checkout dbc9c345de124eecad564e5de1259d7ca4eaf99b + O f777ecc (master) create initial.txt + | + @ dbc9c34 create test3.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + test2.txt + test3.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_fixup_multiple_into_head_with_unmoved_parent() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let _test1_oid = git.commit_file("test1", 1)?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let _test4_oid = git.commit_file("test4", 4)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + @ 355e173 create test4.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test4.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--fixup", + "-x", + &format!("{}+{}", test2_oid, test3_oid), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ a31f704 create test4.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test3.txt + test4.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-x", + &format!("{}+{}", test2_oid, test3_oid), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/3] Committed as: bf0d52a create test4.txt + [2/3] Amended as: 4df8c5d create test4.txt + [3/3] Amended as: a31f704 create test4.txt + branchless: processing 3 rewritten commits + branchless: running command: checkout a31f7045a44661fc911986de7a3164da1fb50420 + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + @ a31f704 create test4.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test3.txt + test4.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_fixup_multiple_into_parent_with_unmoved_head() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let test1_oid = git.commit_file("test1", 1)?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let _test4_oid = git.commit_file("test4", 4)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + @ 355e173 create test4.txt + "###); + + let (stdout, _stderr) = git.run(&[ + "show", + "--pretty=format:", + "--name-only", + &test1_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--fixup", + "-x", + &format!("{}+{}", test2_oid, test3_oid), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 9ede80c create test1.txt + | + @ 0b465e8 create test4.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD~"])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + test2.txt + test3.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-x", + &format!("{}+{}", test2_oid, test3_oid), + "-d", + &test1_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/4] Committed as: 62fc20d create test1.txt + [2/4] Amended as: e9f8a2b create test1.txt + [3/4] Amended as: 9ede80c create test1.txt + [4/4] Committed as: 0b465e8 create test4.txt + branchless: processing 4 rewritten commits + branchless: running command: checkout 0b465e8fb1992620af8d4c0a44ea2c881ea2324d + O f777ecc (master) create initial.txt + | + o 9ede80c create test1.txt + | + @ 0b465e8 create test4.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD~"])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + test2.txt + test3.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_fixup_tree() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let _test1_oid = git.commit_file("test1", 1)?; + let test2_oid = git.commit_file("test2", 2)?; + git.run(&["checkout", "HEAD~"])?; + let _test3_oid = git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + let _test5_oid = git.commit_file("test5", 5)?; + git.run(&["checkout", "HEAD~2"])?; + let test6_oid = git.commit_file("test6", 6)?; + let _test7_oid = git.commit_file("test7", 7)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + |\ + | o 96d1c37 create test2.txt + | + o 4838e49 create test3.txt + |\ + | o a248207 create test4.txt + | | + | o 566e434 create test5.txt + | + o 67f8d55 create test6.txt + | + @ f6e2f75 create test7.txt + "###); + + let (stdout, _stderr) = git.run(&[ + "show", + "--pretty=format:", + "--name-only", + &test4_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + test4.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--fixup", + "-x", + &format!("{}+{}", test2_oid, test6_oid), + "-d", + &test4_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 4838e49 create test3.txt + |\ + | o 9866190 create test4.txt + | | + | o a84ae82 create test5.txt + | + @ dd38f3d create test7.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "9866190"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test4.txt + test6.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-x", + &format!("{}+{}", test2_oid, test6_oid), + "-d", + &test4_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/5] Committed as: a248207 create test4.txt + [2/5] Amended as: 87dd099 create test4.txt + [3/5] Amended as: 9866190 create test4.txt + [4/5] Committed as: a84ae82 create test5.txt + [5/5] Committed as: dd38f3d create test7.txt + branchless: processing 5 rewritten commits + branchless: running command: checkout dd38f3de5f15c714a553870ac93c3472588b511c + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 4838e49 create test3.txt + |\ + | o 9866190 create test4.txt + | | + | o a84ae82 create test5.txt + | + @ dd38f3d create test7.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "9866190"])?; + insta::assert_snapshot!(stdout, @r###" + test2.txt + test4.txt + test6.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_fixup_multiple_disconnected_into_parent() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + git.detach_head()?; + let test1_oid = git.commit_file("test1", 1)?; + let _test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let _test4_oid = git.commit_file("test4", 4)?; + let test5_oid = git.commit_file("test5", 5)?; + let _test6_oid = git.commit_file("test6", 6)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o 62fc20d create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + o 355e173 create test4.txt + | + o f81d55c create test5.txt + | + @ 2831fb5 create test6.txt + "###); + + let (stdout, _stderr) = git.run(&[ + "show", + "--pretty=format:", + "--name-only", + &test1_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--fixup", + "-x", + &format!("{}+{}", test3_oid, test5_oid), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + o f72b5c6 create test1.txt + | + o 921b59b create test2.txt + | + o 61914c5 create test4.txt + | + @ 11fc176 create test6.txt + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "f72b5c6"])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + test3.txt + test5.txt + "###); + } + + // --in-memory + { + let (stdout, _stderr) = git.run(&[ + "move", + "--in-memory", + "--fixup", + "-x", + &format!("{}+{}", test3_oid, test5_oid), + "-d", + &test1_oid.to_string(), + ])?; + insta::assert_snapshot!(stdout, @r###" + Attempting rebase in-memory... + [1/6] Committed as: 62fc20d create test1.txt + [2/6] Amended as: 06748a9 create test1.txt + [3/6] Amended as: f72b5c6 create test1.txt + [4/6] Committed as: 921b59b create test2.txt + [5/6] Committed as: 61914c5 create test4.txt + [6/6] Committed as: 11fc176 create test6.txt + branchless: processing 6 rewritten commits + branchless: running command: checkout 11fc1760ee0f9cebd9d2b7fb58ce7caf1c3056c0 + O f777ecc (master) create initial.txt + | + o f72b5c6 create test1.txt + | + o 921b59b create test2.txt + | + o 61914c5 create test4.txt + | + @ 11fc176 create test6.txt + In-memory rebase succeeded. + "###); + + let (stdout, _stderr) = git.run(&["show", "--pretty=format:", "--name-only", "HEAD~3"])?; + insta::assert_snapshot!(stdout, @r###" + test1.txt + test3.txt + test5.txt + "###); + } + + Ok(()) +}