From ceb6ad54595778805c2d9e4565447b14035c5057 Mon Sep 17 00:00:00 2001 From: Clayton Carter Date: Mon, 11 Jul 2022 21:14:13 -0400 Subject: [PATCH] feat(move): Add `--exact` flag to move sets of commits (close #176) --- git-branchless/src/commands/mod.rs | 2 + git-branchless/src/commands/move.rs | 167 ++- git-branchless/src/opts.rs | 11 + git-branchless/tests/command/test_move.rs | 1255 +++++++++++++++++++-- 4 files changed, 1329 insertions(+), 106 deletions(-) diff --git a/git-branchless/src/commands/mod.rs b/git-branchless/src/commands/mod.rs index 148c4d897..a24e275ba 100644 --- a/git-branchless/src/commands/mod.rs +++ b/git-branchless/src/commands/mod.rs @@ -209,6 +209,7 @@ fn do_main_and_drop_locals() -> eyre::Result { source, dest, base, + exact, insert, move_options, } => r#move::r#move( @@ -217,6 +218,7 @@ fn do_main_and_drop_locals() -> eyre::Result { source, dest, base, + exact, insert, &move_options, )?, diff --git a/git-branchless/src/commands/move.rs b/git-branchless/src/commands/move.rs index 77a2ab618..231f596ab 100644 --- a/git-branchless/src/commands/move.rs +++ b/git-branchless/src/commands/move.rs @@ -3,6 +3,7 @@ //! Under the hood, this makes use of Git's advanced rebase functionality, which //! is also used to preserve merge commits using the `--rebase-merges` option. +use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::Write; use std::time::SystemTime; @@ -16,7 +17,7 @@ use tracing::instrument; use crate::opts::{MoveOptions, Revset}; use crate::revset::resolve_commits; use lib::core::config::get_restack_preserve_timestamps; -use lib::core::dag::{commit_set_to_vec_unsorted, union_all, CommitSet, Dag}; +use lib::core::dag::{commit_set_to_vec_unsorted, sorted_commit_set, union_all, CommitSet, Dag}; use lib::core::effects::Effects; use lib::core::eventlog::{EventLogDb, EventReplayer}; use lib::core::rewrite::{ @@ -59,10 +60,12 @@ pub fn r#move( sources: Vec, dest: Option, bases: Vec, + exacts: Vec, insert: bool, move_options: &MoveOptions, ) -> eyre::Result { - let should_sources_default_to_head = sources.is_empty() && bases.is_empty(); + let should_sources_default_to_head = + sources.is_empty() && bases.is_empty() && exacts.is_empty(); let repo = Repo::from_current_dir()?; let head_oid = repo.get_head_info()?.oid; @@ -105,6 +108,52 @@ pub fn r#move( return Ok(ExitCode(1)); } }; + let exact_components = match resolve_commits(effects, &repo, &mut dag, exacts) { + Ok(commit_sets) => { + let exact_oids = union_all(&commit_sets); + let connected_components = dag.get_connected_components(&exact_oids)?; + let mut components: HashMap = HashMap::new(); + + for component in connected_components.into_iter() { + let component_roots = dag.query().roots(component.clone())?; + let component_root = match commit_set_to_vec_unsorted(&component_roots)?.as_slice() + { + [only_commit_oid] => *only_commit_oid, + _ => { + writeln!( + effects.get_error_stream(), + "The --exact flag can only be used to move ranges with exactly 1 root.\n\ + Received range with {} roots: {:?}", + component_roots.count()?, + component_roots + )?; + return Ok(ExitCode(1)); + } + }; + + let component_heads = dag.query().heads(component.clone())?; + if component_heads.count()? != 1 { + writeln!( + effects.get_error_stream(), + "The --exact flag can only be used to move ranges with exactly 1 head.\n\ + Received range with {} heads: {:?}", + component_heads.count()?, + component_heads + )?; + return Ok(ExitCode(1)); + }; + + components.insert(component_root, component); + } + + components + } + Err(err) => { + err.describe(effects)?; + return Ok(ExitCode(1)); + } + }; + let dest_oid: NonZeroOid = match resolve_commits(effects, &repo, &mut dag, vec![dest.clone()]) { Ok(commit_sets) => match commit_set_to_vec_unsorted(&commit_sets[0])?.as_slice() { [only_commit_oid] => *only_commit_oid, @@ -168,10 +217,117 @@ pub fn r#move( builder.move_subtree(source_root, dest_oid)?; } + let component_roots: CommitSet = exact_components.keys().cloned().collect(); + let component_roots: Vec = sorted_commit_set(&repo, &dag, &component_roots)? + .iter() + .map(|commit| commit.get_oid()) + .collect(); + for component_root in component_roots.iter() { + let component = exact_components.get(component_root).unwrap(); + // We've already established that each component only has 1 root and 1 + // head, so we can be a bit cavelier w/ our Ok/Err handling + let component_head = + NonZeroOid::try_from(dag.query().heads(component.clone())?.first()?.unwrap())?; + + // Find the non-inclusive ancestor components of the current root + let mut possible_destinations: Vec = vec![]; + for root in component_roots.iter() { + let component = exact_components.get(root).unwrap(); + if !component.contains(&(*component_root).into())? + && dag + .query() + .is_ancestor((*root).into(), (*component_root).into())? + { + possible_destinations.push(*root); + } + } + + let component_dest_oid = if possible_destinations.is_empty() { + dest_oid + } else { + // If there was a merge commit somewhere outside of the selected + // components, then it's possible that the current component + // could have multiple possible parents. + // + // To check for this, we can confirm that the nearest + // destination component is an ancestor of the previous (ie next + // nearest). This works because possible_destinations is made + // from component_roots, which has been sorted topologically; so + // each included component should "come after" the previous + // component. + for i in 1..possible_destinations.len() { + if !dag.query().is_ancestor( + possible_destinations[i - 1].into(), + possible_destinations[i].into(), + )? { + writeln!( + effects.get_output_stream(), + "This operation cannot be completed because the {} at {}\n\ + has multiple possible parents also being moved. Please retry this operation\n\ + without this {}, or with only 1 possible parent.", + if component.count()? == 1 { + "commit" + } else { + "range of commits rooted" + }, + component_root, + if component.count()? == 1 { + "commit" + } else { + "range of commits" + }, + )?; + return Ok(ExitCode(1)); + } + } + + let nearest_component = exact_components + .get(&possible_destinations[possible_destinations.len() - 1]) + .unwrap(); + // The current component could be descended from any commit + // in nearest_component, not just it's head. + let dest_ancestor = dag + .query() + .ancestors(CommitSet::from(component_head))? + .intersection(nearest_component); + match dag.query().heads(dest_ancestor.clone())?.first()? { + Some(head) => NonZeroOid::try_from(head)?, + None => dest_oid, + } + }; + + let component_parent = { + let component_parents = dag.query().parents(CommitSet::from(*component_root))?; + match commit_set_to_vec_unsorted(&component_parents)?.as_slice() { + [oid] => *oid, + _ => { + writeln!( + effects.get_output_stream(), + "The --exact flag can only be used to move ranges or commits with exactly 1 parent.", + )?; + return Ok(ExitCode(1)); + } + } + }; + + let component_children: CommitSet = dag + .query() + .children(component.clone())? + .difference(component) + .difference(&dag.obsolete_commits); + + for component_child in commit_set_to_vec_unsorted(&component_children)? { + builder.move_subtree(component_child, component_parent)?; + } + + builder.move_range(*component_root, component_head, component_dest_oid)?; + } + if insert { let source_head = { - let source_heads: CommitSet = - dag.query().heads(dag.query().descendants(source_oids.clone())?)?; + let source_heads: CommitSet = dag + .query() + .heads(dag.query().descendants(source_oids.clone())?)?; match commit_set_to_vec_unsorted(&source_heads)?[..] { [oid] => oid, _ => { @@ -197,6 +353,9 @@ pub fn r#move( .query() .is_ancestor(dest_child.into(), source_root.into())? { + // FIXME this is moving sibling commits up to dest_oid + // but should be leaving them in place + // If this child subtree actually contains the source // subtree being moved, then we should only move the commit // range *up to* the source subtree, not the entire child diff --git a/git-branchless/src/opts.rs b/git-branchless/src/opts.rs index 5082bf25c..2f211c89f 100644 --- a/git-branchless/src/opts.rs +++ b/git-branchless/src/opts.rs @@ -291,6 +291,17 @@ pub enum Command { )] base: Vec, + /// A set of specific commits to move. These will be removed from their + /// current locations and any unmoved children will be moved to their + /// nearest unmoved ancestor. + #[clap( + action(clap::ArgAction::Append), + short = 'x', + long = "exact", + conflicts_with_all(&["source", "base"]) + )] + exact: Vec, + /// The destination commit to move all source commits onto. If not /// provided, defaults to the current commit. #[clap(value_parser, short = 'd', long = "dest")] diff --git a/git-branchless/tests/command/test_move.rs b/git-branchless/tests/command/test_move.rs index b404afe2d..ec97f2204 100644 --- a/git-branchless/tests/command/test_move.rs +++ b/git-branchless/tests/command/test_move.rs @@ -252,22 +252,29 @@ fn test_move_insert_stick() -> eyre::Result<()> { } #[test] -fn test_move_tree() -> eyre::Result<()> { +fn test_move_exact_single_stick() -> eyre::Result<()> { let git = make_git()?; - if !git.supports_committer_date_is_author_date()? { return Ok(()); } git.init_repo()?; - let test1_oid = git.commit_file("test1", 1)?; - git.commit_file("test2", 2)?; - git.detach_head()?; + git.commit_file("test2", 2)?; let test3_oid = git.commit_file("test3", 3)?; git.commit_file("test4", 4)?; - git.run(&["checkout", &test3_oid.to_string()])?; - git.commit_file("test5", 5)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + @ 355e173 create test4.txt + "###); // --on-disk { @@ -275,97 +282,129 @@ fn test_move_tree() -> eyre::Result<()> { git.run(&[ "move", "--on-disk", - "-s", + "--exact", &test3_oid.to_string(), "-d", &test1_oid.to_string(), ])?; - { - let (stdout, _stderr) = git.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - O 62fc20d create test1.txt - |\ - | o 4838e49 create test3.txt - | |\ - | | o a248207 create test4.txt - | | - | @ b1f9efa create test5.txt - | - O 96d1c37 (master) create test2.txt - "###); - } + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | @ f57e36f create test4.txt + | + o 4838e49 create test3.txt + "###); } - // in-memory + // --in-memory { git.run(&[ "move", - "-s", + "--in-memory", + "--exact", &test3_oid.to_string(), "-d", &test1_oid.to_string(), ])?; - { - let (stdout, _stderr) = git.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - O 62fc20d create test1.txt - |\ - | o 4838e49 create test3.txt - | |\ - | | o a248207 create test4.txt - | | - | @ b1f9efa create test5.txt - | - O 96d1c37 (master) create test2.txt - "###); - } + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | @ f57e36f create test4.txt + | + o 4838e49 create test3.txt + "###); } Ok(()) } #[test] -fn test_move_insert_in_place() -> eyre::Result<()> { +fn test_move_exact_range_stick() -> eyre::Result<()> { let git = make_git()?; if !git.supports_committer_date_is_author_date()? { return Ok(()); } git.init_repo()?; - let test1_oid = git.commit_file("test1", 1)?; + git.commit_file("test1", 1)?; git.detach_head()?; let test2_oid = git.commit_file("test2", 2)?; - git.run(&["checkout", "HEAD^"])?; - git.commit_file("test3", 3)?; - + let test3_oid = git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + git.commit_file("test5", 5)?; let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : O 62fc20d (master) create test1.txt - |\ - | o 96d1c37 create test2.txt | - @ 4838e49 create test3.txt + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + o 355e173 create test4.txt + | + @ f81d55c create test5.txt "###); - git.run(&[ - "move", - "--insert", - "-s", - &test2_oid.to_string(), - "-d", - &test1_oid.to_string(), - ])?; + // --on-disk { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--exact", + &format!("{}:{}", test2_oid, test3_oid), + "-d", + &test4_oid.to_string(), + ])?; + let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : O 62fc20d (master) create test1.txt | - o 96d1c37 create test2.txt + o bf0d52a create test4.txt + |\ + | o 44352d0 create test2.txt + | | + | o cf5eb24 create test3.txt | - @ 70deb1e create test3.txt + @ 848121c create test5.txt + "###); + } + + // --in-memory + { + git.run(&[ + "move", + "--in-memory", + "--exact", + &format!("{}:{}", test2_oid, test3_oid), + "-d", + &test4_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o bf0d52a create test4.txt + |\ + | o 44352d0 create test2.txt + | | + | o cf5eb24 create test3.txt + | + @ 848121c create test5.txt "###); } @@ -373,32 +412,40 @@ fn test_move_insert_in_place() -> eyre::Result<()> { } #[test] -fn test_move_insert_tree() -> eyre::Result<()> { +fn test_move_exact_noncontiguous_singles_stick() -> eyre::Result<()> { let git = make_git()?; - if !git.supports_committer_date_is_author_date()? { return Ok(()); } git.init_repo()?; - let test1_oid = git.commit_file("test1", 1)?; git.detach_head()?; git.commit_file("test2", 2)?; - - git.run(&["checkout", &test1_oid.to_string()])?; - git.commit_file("test3", 3)?; - let test4_oid = git.commit_file("test4", 4)?; + let test3_oid = git.commit_file("test3", 3)?; + git.commit_file("test4", 4)?; + let test5_oid = git.commit_file("test5", 5)?; + git.commit_file("test6", 6)?; + let test7_oid = git.commit_file("test7", 7)?; + git.commit_file("test8", 8)?; let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : O 62fc20d (master) create test1.txt - |\ - | o 96d1c37 create test2.txt | - o 4838e49 create test3.txt + o 96d1c37 create test2.txt | - @ a248207 create test4.txt + o 70deb1e create test3.txt + | + o 355e173 create test4.txt + | + o f81d55c create test5.txt + | + o 2831fb5 create test6.txt + | + o c8933b3 create test7.txt + | + @ 1edbaa1 create test8.txt "###); // --on-disk @@ -407,51 +454,1055 @@ fn test_move_insert_tree() -> eyre::Result<()> { git.run(&[ "move", "--on-disk", - "--insert", - "-s", - &test4_oid.to_string(), + "--exact", + &format!("{} + {} + {}", test3_oid, test5_oid, test7_oid), "-d", &test1_oid.to_string(), ])?; - { - let (stdout, _stderr) = git.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - O 62fc20d (master) create test1.txt - | - @ bf0d52a create test4.txt - |\ - | o 44352d0 create test2.txt - | - o 0a4a701 create test3.txt - "###); - } + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o f57e36f create test4.txt + | | + | o d0c8705 create test6.txt + | | + | @ c458f41 create test8.txt + | + o 4838e49 create test3.txt + | + o b1f9efa create test5.txt + | + o 8577a96 create test7.txt + "###); } - // in-memory + // --in-memory { git.run(&[ "move", - "--insert", - "-s", - &test4_oid.to_string(), + "--in-memory", + "--exact", + &format!("{} + {} + {}", test3_oid, test5_oid, test7_oid), "-d", &test1_oid.to_string(), ])?; - { - let (stdout, _stderr) = git.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - O 62fc20d (master) create test1.txt - | - @ bf0d52a create test4.txt - |\ - | o 44352d0 create test2.txt - | - o 0a4a701 create test3.txt - "###); - } + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o f57e36f create test4.txt + | | + | o d0c8705 create test6.txt + | | + | @ c458f41 create test8.txt + | + o 4838e49 create test3.txt + | + o b1f9efa create test5.txt + | + o 8577a96 create test7.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_exact_noncontiguous_ranges_stick() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + git.commit_file("test5", 5)?; + let test6_oid = git.commit_file("test6", 6)?; + let test7_oid = git.commit_file("test7", 7)?; + git.commit_file("test8", 8)?; + let test9_oid = git.commit_file("test9", 9)?; + let test10_oid = git.commit_file("test10", 10)?; + git.commit_file("test11", 11)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + o 355e173 create test4.txt + | + o f81d55c create test5.txt + | + o 2831fb5 create test6.txt + | + o c8933b3 create test7.txt + | + o 1edbaa1 create test8.txt + | + o 384010f create test9.txt + | + o 52ebfa0 create test10.txt + | + @ b22a15b create test11.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--exact", + &format!( + "{}:{} + {}:{} + {}:{}", + test3_oid, test4_oid, test6_oid, test7_oid, test9_oid, test10_oid + ), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o d2e18e3 create test5.txt + | | + | o a50e00a create test8.txt + | | + | @ dceb23f create test11.txt + | + o 4838e49 create test3.txt + | + o a248207 create test4.txt + | + o 133f783 create test6.txt + | + o c603422 create test7.txt + | + o 9c7387c create test9.txt + | + o fed2ec4 create test10.txt + "###); + } + + // --in-memory + { + git.run(&[ + "move", + "--in-memory", + "--exact", + &format!( + "{}:{} + {}:{} + {}:{}", + test3_oid, test4_oid, test6_oid, test7_oid, test9_oid, test10_oid + ), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o d2e18e3 create test5.txt + | | + | o a50e00a create test8.txt + | | + | @ dceb23f create test11.txt + | + o 4838e49 create test3.txt + | + o a248207 create test4.txt + | + o 133f783 create test6.txt + | + o c603422 create test7.txt + | + o 9c7387c create test9.txt + | + o fed2ec4 create test10.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_exact_contiguous_and_noncontiguous_stick() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + git.commit_file("test4", 4)?; + let test5_oid = git.commit_file("test5", 5)?; + let test6_oid = git.commit_file("test6", 6)?; + git.commit_file("test7", 7)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + | + o 355e173 create test4.txt + | + o f81d55c create test5.txt + | + o 2831fb5 create test6.txt + | + @ c8933b3 create test7.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--exact", + &format!("{} + {}:{}", test3_oid, test5_oid, test6_oid), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o f57e36f create test4.txt + | | + | @ c538d3e create test7.txt + | + o 4838e49 create test3.txt + | + o b1f9efa create test5.txt + | + o 500c9b3 create test6.txt + "###); + } + + // --in-memory + { + git.run(&[ + "move", + "--in-memory", + "--exact", + &format!("{} + {}:{}", test3_oid, test5_oid, test6_oid), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o f57e36f create test4.txt + | | + | @ c538d3e create test7.txt + | + o 4838e49 create test3.txt + | + o b1f9efa create test5.txt + | + o 500c9b3 create test6.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_tree() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + let test1_oid = git.commit_file("test1", 1)?; + git.commit_file("test2", 2)?; + + git.detach_head()?; + let test3_oid = git.commit_file("test3", 3)?; + git.commit_file("test4", 4)?; + git.run(&["checkout", &test3_oid.to_string()])?; + git.commit_file("test5", 5)?; + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "-s", + &test3_oid.to_string(), + "-d", + &test1_oid.to_string(), + ])?; + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d create test1.txt + |\ + | o 4838e49 create test3.txt + | |\ + | | o a248207 create test4.txt + | | + | @ b1f9efa create test5.txt + | + O 96d1c37 (master) create test2.txt + "###); + } + } + + // in-memory + { + git.run(&[ + "move", + "-s", + &test3_oid.to_string(), + "-d", + &test1_oid.to_string(), + ])?; + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d create test1.txt + |\ + | o 4838e49 create test3.txt + | |\ + | | o a248207 create test4.txt + | | + | @ b1f9efa create test5.txt + | + O 96d1c37 (master) create test2.txt + "###); + } + } + + Ok(()) +} + +#[test] +fn test_move_insert_in_place() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + git.run(&["checkout", "HEAD^"])?; + git.commit_file("test3", 3)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | + @ 4838e49 create test3.txt + "###); + + git.run(&[ + "move", + "--insert", + "-s", + &test2_oid.to_string(), + "-d", + &test1_oid.to_string(), + ])?; + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + @ 70deb1e create test3.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_insert_tree() -> eyre::Result<()> { + let git = make_git()?; + + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + git.commit_file("test2", 2)?; + + git.run(&["checkout", &test1_oid.to_string()])?; + 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 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | + o 4838e49 create test3.txt + | + @ a248207 create test4.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--insert", + "-s", + &test4_oid.to_string(), + "-d", + &test1_oid.to_string(), + ])?; + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + @ bf0d52a create test4.txt + |\ + | o 44352d0 create test2.txt + | + o 0a4a701 create test3.txt + "###); + } + } + + // in-memory + { + git.run(&[ + "move", + "--insert", + "-s", + &test4_oid.to_string(), + "-d", + &test1_oid.to_string(), + ])?; + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + @ bf0d52a create test4.txt + |\ + | o 44352d0 create test2.txt + | + o 0a4a701 create test3.txt + "###); + } + } + + Ok(()) +} + +#[test] +fn test_move_exact_range_tree() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + git.run(&["checkout", &test3_oid.to_string()])?; + git.commit_file("test5", 5)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + |\ + | o 355e173 create test4.txt + | + @ 9ea1b36 create test5.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--exact", + &format!("{}:{}", test2_oid, test3_oid), + "-d", + &test4_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o bf0d52a create test4.txt + | | + | o 44352d0 create test2.txt + | | + | o cf5eb24 create test3.txt + | + @ ea7aa06 create test5.txt + "###); + } + + // in-memory + { + git.run(&[ + "move", + "--exact", + &format!("{}:{}", test2_oid, test3_oid), + "-d", + &test4_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o bf0d52a create test4.txt + | | + | o 44352d0 create test2.txt + | | + | o cf5eb24 create test3.txt + | + @ ea7aa06 create test5.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_exact_range_with_leaves() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + git.commit_file("test4", 4)?; + git.run(&["checkout", &test3_oid.to_string()])?; + let test5_oid = git.commit_file("test5", 5)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + |\ + | o 355e173 create test4.txt + | + @ 9ea1b36 create test5.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--exact", + &format!("{}+{}", test3_oid, test5_oid), + "-d", + &test2_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | | + | @ 9ea1b36 create test5.txt + | + o f57e36f create test4.txt + "###); + } + + // in-memory + { + git.run(&[ + "move", + "--exact", + &format!("{}+{}", test3_oid, test5_oid), + "-d", + &test2_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | | + | @ 9ea1b36 create test5.txt + | + o f57e36f create test4.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_exact_range_with_leaves_and_descendent_components() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + git.commit_file("test4", 4)?; + let test5_oid = git.commit_file("test5", 5)?; + git.run(&["checkout", &test3_oid.to_string()])?; + let test6_oid = git.commit_file("test6", 6)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + | + o 70deb1e create test3.txt + |\ + | o 355e173 create test4.txt + | | + | o f81d55c create test5.txt + | + @ eaf39e9 create test6.txt + "###); + + // --on-disk + { + let git = git.duplicate_repo()?; + git.run(&[ + "move", + "--on-disk", + "--exact", + &format!("{}+{}+{}", test3_oid, test5_oid, test6_oid), + "-d", + &test2_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | |\ + | | o 9ea1b36 create test5.txt + | | + | @ eaf39e9 create test6.txt + | + o f57e36f create test4.txt + "###); + } + + // in-memory + { + git.run(&[ + "move", + "--exact", + &format!("{}+{}+{}", test3_oid, test5_oid, test6_oid), + "-d", + &test2_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | |\ + | | o 9ea1b36 create test5.txt + | | + | @ eaf39e9 create test6.txt + | + o f57e36f create test4.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_move_exact_ranges_with_merge_commits_betwixt_not_supported() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + git.run(&["checkout", &test1_oid.to_string()])?; + let test4_oid = git.commit_file("test4", 4)?; + git.commit_file("test5", 5)?; + git.run(&["merge", &test3_oid.to_string()])?; + let test6_oid = git.commit_file("test6", 6)?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | | + | o 70deb1e create test3.txt + | | + | o 01a3b9b Merge commit '70deb1e28791d8e7dd5a1f0c871a51b91282562f' into HEAD + | | + | @ 8fb4e4a create test6.txt + | + o bf0d52a create test4.txt + | + o 848121c create test5.txt + | + o 01a3b9b Merge commit '70deb1e28791d8e7dd5a1f0c871a51b91282562f' into HEAD + | + @ 8fb4e4a create test6.txt + "###); + + let (stdout, stderr) = git.run_with_options( + &[ + "move", + "--exact", + &format!("{}+{}+{}", test2_oid, test4_oid, test6_oid), + "-d", + &test2_oid.to_string(), + ], + &GitRunOptions { + expected_exit_code: 1, + ..Default::default() + }, + )?; + insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stdout, @r###" + This operation cannot be completed because the commit at 8fb4e4aded06dd3b97723794474832a928370f9a + has multiple possible parents also being moved. Please retry this operation + without this commit, or with only 1 possible parent. + "###); + + Ok(()) +} + +#[test] +fn test_move_exact_range_one_side_of_merged_stack_without_base_and_merge_commits( +) -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + git.run(&["checkout", &test2_oid.to_string()])?; + git.commit_file("test5", 5)?; + git.commit_file("test6", 6)?; + git.run(&["merge", &test4_oid.to_string()])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | | + | o 355e173 create test4.txt + | | + | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | + o d2e18e3 create test5.txt + | + o d43fec8 create test6.txt + | + @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + "###); + + git.run(&[ + "move", + "--exact", + &format!("{}+{}", test3_oid, test4_oid), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + // FIXME: This output is correct except for a known issue involving moving + // merge commits. (See `test_move_merge_commit_both_parents`.) + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | |\ + | | x 70deb1e (rewritten as 4838e49b) create test3.txt + | | | + | | x 355e173 (rewritten as a2482074) create test4.txt + | | | + | | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | | + | o d2e18e3 create test5.txt + | | + | o d43fec8 create test6.txt + | | + | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | + o 4838e49 create test3.txt + | + o a248207 create test4.txt + "###); + + Ok(()) +} + +#[test] +fn test_move_exact_range_one_side_of_merged_stack_including_base_and_merge_commits( +) -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + let test3_oid = git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + git.run(&["checkout", &test2_oid.to_string()])?; + git.commit_file("test5", 5)?; + git.commit_file("test6", 6)?; + git.run(&["merge", &test4_oid.to_string()])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | | + | o 355e173 create test4.txt + | | + | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | + o d2e18e3 create test5.txt + | + o d43fec8 create test6.txt + | + @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + "###); + + git.run(&[ + "move", + "--exact", + &format!("{}+{}+{}+{}", test2_oid, test3_oid, test4_oid, "178e00f"), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + // FIXME: This output is correct except for a known issue involving moving + // merge commits. (See `test_move_merge_commit_both_parents`.) + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | |\ + | | o 70deb1e create test3.txt + | | | + | | o 355e173 create test4.txt + | | | + | | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | | + | x d2e18e3 (rewritten as ea7aa064) create test5.txt + | | + | x d43fec8 (rewritten as da42aeb4) create test6.txt + | | + | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | + o ea7aa06 create test5.txt + | + o da42aeb create test6.txt + "###); + + Ok(()) +} + +#[test] +fn test_move_exact_range_two_partial_components_of_merged_stack() -> eyre::Result<()> { + let git = make_git()?; + if !git.supports_committer_date_is_author_date()? { + return Ok(()); + } + git.init_repo()?; + let test1_oid = git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + git.commit_file("test3", 3)?; + let test4_oid = git.commit_file("test4", 4)?; + git.run(&["checkout", &test2_oid.to_string()])?; + let test5_oid = git.commit_file("test5", 5)?; + git.commit_file("test6", 6)?; + git.run(&["merge", &test4_oid.to_string()])?; + + // Given this graph: 1-2-3-4-7 + // \5-6/ + // Moving 2,3,6,7 (leaving 4,5) should produce: + // 1-2-3 + // | \6-7 + // +4 + // \5 + // FIXME Is it Ok that 3&7 are no longer directly connected? + + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + | + o 96d1c37 create test2.txt + |\ + | o 70deb1e create test3.txt + | | + | o 355e173 create test4.txt + | | + | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | + o d2e18e3 create test5.txt + | + o d43fec8 create test6.txt + | + @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + "###); + + git.run(&[ + "move", + "--exact", + &format!("{}:: - {} - {}", test2_oid, test4_oid, test5_oid), + "-d", + &test1_oid.to_string(), + ])?; + + let (stdout, _stderr) = git.run(&["smartlog"])?; + // FIXME: This output is correct except for a known issue involving moving + // merge commits. (See `test_move_merge_commit_both_parents`.) + insta::assert_snapshot!(stdout, @r###" + : + O 62fc20d (master) create test1.txt + |\ + | o 96d1c37 create test2.txt + | |\ + | | o 70deb1e create test3.txt + | | |\ + | | | x 355e173 (rewritten as bf0d52a6) create test4.txt + | | | | + | | | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + | | | + | | o eaf39e9 create test6.txt + | | + | x d2e18e3 (rewritten as ea7aa064) create test5.txt + | | + | x d43fec8 (rewritten as eaf39e91) create test6.txt + | | + | @ 178e00f Merge commit '355e173bf9c5d2efac2e451da0cdad3fb82b869a' into HEAD + |\ + | o bf0d52a create test4.txt + | + o ea7aa06 create test5.txt + "###); Ok(()) }