From 76654a2837cfd480e8f671c6b5912d1efefdce3a Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 2 Jun 2024 02:39:22 +0800 Subject: [PATCH] duplicate: add `--destination`, `--insert-after`, and `--insert-before` options --- CHANGELOG.md | 4 + cli/src/commands/duplicate.rs | 204 ++- cli/tests/cli-reference@.md.snap | 8 +- cli/tests/test_duplicate_command.rs | 2049 +++++++++++++++++++++++++++ docs/git-comparison.md | 3 +- 5 files changed, 2242 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec9be3750..b2861e64a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * New template functions `pad_start()`, `pad_end()`, `truncate_start()`, and `truncate_end()` are added. +* `jj duplicate` now accepts `--destination`, `--insert-after` and + `--insert-before` options to customize the location of the duplicated + revisions. + ### Fixed bugs * Error on `trunk()` revset resolution is now handled gracefully. diff --git a/cli/src/commands/duplicate.rs b/cli/src/commands/duplicate.rs index c229ca0c46..f18d209071 100644 --- a/cli/src/commands/duplicate.rs +++ b/cli/src/commands/duplicate.rs @@ -13,12 +13,18 @@ // limitations under the License. use std::io::Write; +use std::rc::Rc; -use indexmap::IndexMap; +use clap::ArgGroup; use itertools::Itertools; use jj_lib::backend::CommitId; -use jj_lib::commit::Commit; +use jj_lib::commit::CommitIteratorExt; +use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; +use jj_lib::revset::RevsetExpression; +use jj_lib::rewrite::duplicate_commits; +use jj_lib::rewrite::DuplicateCommitsDestination; +use jj_lib::rewrite::DuplicateCommitsResult; use tracing::instrument; use crate::cli_util::short_commit_hash; @@ -30,6 +36,7 @@ use crate::ui::Ui; /// Create a new change with the same content as an existing one #[derive(clap::Args, Clone, Debug)] +#[command(group(ArgGroup::new("target").args(&["destination", "insert_after", "insert_before"]).multiple(true)))] pub(crate) struct DuplicateArgs { /// The revision(s) to duplicate #[arg(default_value = "@")] @@ -37,6 +44,28 @@ pub(crate) struct DuplicateArgs { /// Ignored (but lets you pass `-r` for consistency with other commands) #[arg(short = 'r', hide = true, action = clap::ArgAction::Count)] unused_revision: u8, + /// The revision(s) to duplicate onto (can be repeated to create a merge + /// commit) + #[arg(long, short)] + destination: Vec, + /// The revision(s) to insert after (can be repeated to create a merge + /// commit) + #[arg( + long, + short = 'A', + visible_alias = "after", + conflicts_with = "destination" + )] + insert_after: Vec, + /// The revision(s) to insert before (can be repeated to create a merge + /// commit) + #[arg( + long, + short = 'B', + visible_alias = "before", + conflicts_with = "destination" + )] + insert_before: Vec, } #[instrument(skip_all)] @@ -57,37 +86,166 @@ pub(crate) fn cmd_duplicate( if to_duplicate.last() == Some(workspace_command.repo().store().root_commit_id()) { return Err(user_error("Cannot duplicate the root commit")); } - let mut duplicated_old_to_new: IndexMap<&CommitId, Commit> = IndexMap::new(); - let mut tx = workspace_command.start_transaction(); - let base_repo = tx.base_repo().clone(); - let store = base_repo.store(); - let mut_repo = tx.repo_mut(); + let parent_commit_ids: Vec; + let children_commit_ids: Vec; - for original_commit_id in to_duplicate.iter().rev() { - // Topological order ensures that any parents of `original_commit` are - // either not in `to_duplicate` or were already duplicated. - let original_commit = store.get_commit(original_commit_id)?; - let new_parents = original_commit - .parent_ids() + if !args.insert_before.is_empty() && !args.insert_after.is_empty() { + let parent_commits = workspace_command + .resolve_some_revsets_default_single(ui, &args.insert_after)? + .into_iter() + .collect_vec(); + parent_commit_ids = parent_commits.iter().ids().cloned().collect(); + let children_commits = workspace_command + .resolve_some_revsets_default_single(ui, &args.insert_before)? + .into_iter() + .collect_vec(); + children_commit_ids = children_commits.iter().ids().cloned().collect(); + workspace_command.check_rewritable(&children_commit_ids)?; + let children_expression = RevsetExpression::commits(children_commit_ids.clone()); + let parents_expression = RevsetExpression::commits(parent_commit_ids.clone()); + ensure_no_commit_loop( + workspace_command.repo(), + &children_expression, + &parents_expression, + )?; + } else if !args.insert_before.is_empty() { + let children_commits = workspace_command + .resolve_some_revsets_default_single(ui, &args.insert_before)? + .into_iter() + .collect_vec(); + children_commit_ids = children_commits.iter().ids().cloned().collect(); + workspace_command.check_rewritable(&children_commit_ids)?; + let children_expression = RevsetExpression::commits(children_commit_ids.clone()); + let parents_expression = children_expression.parents(); + ensure_no_commit_loop( + workspace_command.repo(), + &children_expression, + &parents_expression, + )?; + // Manually collect the parent commit IDs to preserve the order of parents. + parent_commit_ids = children_commits + .iter() + .flat_map(|commit| commit.parent_ids()) + .unique() + .cloned() + .collect_vec(); + } else if !args.insert_after.is_empty() { + let parent_commits = workspace_command + .resolve_some_revsets_default_single(ui, &args.insert_after)? + .into_iter() + .collect_vec(); + parent_commit_ids = parent_commits.iter().ids().cloned().collect(); + let parents_expression = RevsetExpression::commits(parent_commit_ids.clone()); + let children_expression = parents_expression.children(); + children_commit_ids = children_expression + .clone() + .evaluate_programmatic(workspace_command.repo().as_ref()) + .map_err(|err| err.expect_backend_error())? .iter() - .map(|id| duplicated_old_to_new.get(id).map_or(id, |c| c.id()).clone()) - .collect(); - let new_commit = mut_repo - .rewrite_commit(command.settings(), &original_commit) - .generate_new_change_id() - .set_parents(new_parents) - .write()?; - duplicated_old_to_new.insert(original_commit_id, new_commit); + .try_collect()?; + workspace_command.check_rewritable(&children_commit_ids)?; + ensure_no_commit_loop( + workspace_command.repo(), + &children_expression, + &parents_expression, + )?; + } else if !args.destination.is_empty() { + let parent_commits = workspace_command + .resolve_some_revsets_default_single(ui, &args.destination)? + .into_iter() + .collect_vec(); + parent_commit_ids = parent_commits.iter().ids().cloned().collect(); + children_commit_ids = vec![]; + } else { + parent_commit_ids = vec![]; + children_commit_ids = vec![]; + }; + + let mut tx = workspace_command.start_transaction(); + + if !parent_commit_ids.is_empty() { + for commit_id in &to_duplicate { + for parent_commit_id in &parent_commit_ids { + if tx.repo().index().is_ancestor(commit_id, parent_commit_id) { + writeln!( + ui.warning_default(), + "Duplicating commit {} as a descendant of itself", + short_commit_hash(commit_id) + )?; + break; + } + } + } + + for commit_id in &to_duplicate { + for child_commit_id in &children_commit_ids { + if tx.repo().index().is_ancestor(child_commit_id, commit_id) { + writeln!( + ui.warning_default(), + "Duplicating commit {} as an ancestor of itself", + short_commit_hash(commit_id) + )?; + break; + } + } + } } + let num_to_duplicate = to_duplicate.len(); + let DuplicateCommitsResult { + duplicated_commits, + num_rebased, + } = duplicate_commits( + command.settings(), + tx.repo_mut(), + to_duplicate, + if parent_commit_ids.is_empty() { + DuplicateCommitsDestination::Parents + } else { + DuplicateCommitsDestination::Destination { + parent_commit_ids, + children_commit_ids, + } + }, + )?; + if let Some(mut formatter) = ui.status_formatter() { - for (old_id, new_commit) in &duplicated_old_to_new { + for (old_id, new_commit) in &duplicated_commits { write!(formatter, "Duplicated {} as ", short_commit_hash(old_id))?; tx.write_commit_summary(formatter.as_mut(), new_commit)?; writeln!(formatter)?; } + if num_rebased > 0 { + writeln!( + ui.status(), + "Rebased {num_rebased} commits onto duplicated commits" + )?; + } + } + tx.finish(ui, format!("duplicate {num_to_duplicate} commit(s)"))?; + Ok(()) +} + +/// Ensure that there is no possible cycle between the potential children and +/// parents of the duplicated commits. +fn ensure_no_commit_loop( + repo: &ReadonlyRepo, + children_expression: &Rc, + parents_expression: &Rc, +) -> Result<(), CommandError> { + if let Some(commit_id) = children_expression + .dag_range_to(parents_expression) + .evaluate_programmatic(repo)? + .iter() + .next() + { + let commit_id = commit_id?; + return Err(user_error(format!( + "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \ + the duplicated commits", + short_commit_hash(&commit_id), + ))); } - tx.finish(ui, format!("duplicate {} commit(s)", to_duplicate.len()))?; Ok(()) } diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 87e2f2c904..6befe8f944 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -689,7 +689,7 @@ See `jj restore` if you want to move entire files from one revision to another. Create a new change with the same content as an existing one -**Usage:** `jj duplicate [REVISIONS]...` +**Usage:** `jj duplicate [OPTIONS] [REVISIONS]...` ###### **Arguments:** @@ -697,6 +697,12 @@ Create a new change with the same content as an existing one Default value: `@` +###### **Options:** + +* `-d`, `--destination ` — The revision(s) to duplicate onto (can be repeated to create a merge commit) +* `-A`, `--insert-after ` — The revision(s) to insert after (can be repeated to create a merge commit) +* `-B`, `--insert-before ` — The revision(s) to insert before (can be repeated to create a merge commit) + ## `jj edit` diff --git a/cli/tests/test_duplicate_command.rs b/cli/tests/test_duplicate_command.rs index 85d5197c2e..97c998fa81 100644 --- a/cli/tests/test_duplicate_command.rs +++ b/cli/tests/test_duplicate_command.rs @@ -252,6 +252,2055 @@ fn test_duplicate_many() { "###); } +#[test] +fn test_duplicate_destination() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + create_commit(&test_env, &repo_path, "a1", &[]); + create_commit(&test_env, &repo_path, "a2", &["a1"]); + create_commit(&test_env, &repo_path, "a3", &["a2"]); + create_commit(&test_env, &repo_path, "b", &[]); + create_commit(&test_env, &repo_path, "c", &[]); + create_commit(&test_env, &repo_path, "d", &[]); + let setup_opid = test_env.current_operation_id(&repo_path); + + // Test the setup + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ f7550bb42c6f d + │ ○ b75b7aa4b90e c + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + + // Duplicate a single commit onto a single destination. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "-d", "c"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as nkmrtpmo 4587e554 a1 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 4587e554fef9 a1 + ○ b75b7aa4b90e c + │ @ f7550bb42c6f d + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit onto multiple destinations. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "-d", "c", "-d", "d"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as xtnwkqum b82e6252 a1 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ b82e62526e11 a1 + ├─╮ + │ @ f7550bb42c6f d + ○ │ b75b7aa4b90e c + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit onto its descendant. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "-d", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as wvuyspvk 5b3cf5a5 a1 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 5b3cf5a5cbc2 a1 + ○ 17072aa2b823 a3 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ f7550bb42c6f d + ├─╯ + │ ○ b75b7aa4b90e c + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + ◆ 000000000000 + "#); + + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + // Duplicate multiple commits without a direct ancestry relationship onto a + // single destination. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "b", "-d", "c"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as xlzxqlsl 30bff9b1 a1 + Duplicated 9a27d5939bef as vnkwvqxw c7016240 b + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ c7016240cc66 b + │ ○ 30bff9b13575 a1 + ├─╯ + ○ b75b7aa4b90e c + │ @ f7550bb42c6f d + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship onto + // multiple destinations. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "b", "-d", "c", "-d", "d"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as oupztwtk 8fd646d0 a1 + Duplicated 9a27d5939bef as yxsqzptr 7d7269ca b + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 7d7269ca124a b + ├─╮ + │ │ ○ 8fd646d085a9 a1 + ╭─┬─╯ + │ @ f7550bb42c6f d + ○ │ b75b7aa4b90e c + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship onto a + // single destination. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a3", "-d", "c"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as wtszoswq 58411bed a1 + Duplicated 17072aa2b823 as qmykwtmu 86842c96 a3 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 86842c96d8c8 a3 + ○ 58411bed3598 a1 + ○ b75b7aa4b90e c + │ @ f7550bb42c6f d + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship onto + // multiple destinations. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a3", "-d", "c", "-d", "d"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as rkoyqlrv 57d65d68 a1 + Duplicated 17072aa2b823 as zxvrqtmq 144cd2f3 a3 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 144cd2f3a5ab a3 + ○ 57d65d688a47 a1 + ├─╮ + │ @ f7550bb42c6f d + ○ │ b75b7aa4b90e c + ├─╯ + │ ○ 9a27d5939bef b + ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); +} + +#[test] +fn test_duplicate_insert_after() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + create_commit(&test_env, &repo_path, "a1", &[]); + create_commit(&test_env, &repo_path, "a2", &["a1"]); + create_commit(&test_env, &repo_path, "a3", &["a2"]); + create_commit(&test_env, &repo_path, "a4", &["a3"]); + create_commit(&test_env, &repo_path, "b1", &[]); + create_commit(&test_env, &repo_path, "b2", &["b1"]); + create_commit(&test_env, &repo_path, "c1", &[]); + create_commit(&test_env, &repo_path, "c2", &["c1"]); + create_commit(&test_env, &repo_path, "d1", &[]); + create_commit(&test_env, &repo_path, "d2", &["d1"]); + let setup_opid = test_env.current_operation_id(&repo_path); + + // Test the setup + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 0cdd923e993a d2 + ○ 0f21c5e185c5 d1 + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + + // Duplicate a single commit after a single commit with no direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "--after", "b1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as pzsxstzt b34eead0 a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ a384ab7ad1f6 b2 + ○ b34eead0fdf5 a1 + ○ dcc98bc8bbea b1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit after a single ancestor commit. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a3", "--after", "a1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as qmkrwlvp c167d08f a3 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 8746d17a44cb a4 + ○ 15a695f5bf13 a3 + ○ 73e26c9e22e7 a2 + ○ c167d08f8d9f a3 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit after a single descendant commit. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "--after", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as qwyusntz 074debdf a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 3fcf9fdec8f3 a4 + ○ 074debdf330b a1 + ○ 17072aa2b823 a3 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit after multiple commits with no direct + // relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--after", "b1", "--after", "c1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as soqnvnyz 671da6dc a1 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 35ccc31b58bd c2 + │ ○ 7951d1641b4b b2 + ├─╯ + ○ 671da6dc2d2e a1 + ├─╮ + │ ○ b27346e9a9bd c1 + ○ │ dcc98bc8bbea b1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit after multiple commits including an ancestor. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "--after", "a2", "--after", "b2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as nsrwusvy 727c43ec a3 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 5ae709b39efb a4 + ○ ecb0aa61feab a3 + ○ 727c43ec8eaa a3 + ├─╮ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit after multiple commits including a descendant. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--after", "a3", "--after", "b2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as xpnwykqz 6944eeac a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 4fa1dfb1735f a4 + ○ 6944eeac206a a1 + ├─╮ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ○ │ 17072aa2b823 a3 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship after a + // single commit without a direct relationship. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "b1", "--after", "c1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as sryyqqkq d3dda93b a1 + Duplicated dcc98bc8bbea as pxnqtknr 21b26c06 b1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ e9f2b664654b c2 + ├─╮ + │ ○ 21b26c06639f b1 + ○ │ d3dda93b8e6f a1 + ├─╯ + ○ b27346e9a9bd c1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship after a + // single commit which is an ancestor of one of the duplicated commits. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a3", "b1", "--after", "a2"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as pyoswmwk 0d11d466 a3 + Duplicated dcc98bc8bbea as yqnpwwmq f18498f2 b1 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 5b30b2d24181 a4 + ○ 2725567328bd a3 + ├─╮ + │ ○ f18498f24737 b1 + ○ │ 0d11d4667aa9 a3 + ├─╯ + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship after a + // single commit which is a descendant of one of the duplicated commits. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "b1", "--after", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as tpmlxquz b7458ffe a1 + Duplicated dcc98bc8bbea as uukzylyy 7366036f b1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ b19d9559f21a a4 + ├─╮ + │ ○ 7366036f148d b1 + ○ │ b7458ffedb08 a1 + ├─╯ + ○ 17072aa2b823 a3 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship after + // multiple commits without a direct relationship to the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--after", "c1", "--after", "d1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as knltnxnu a276dada a1 + Duplicated dcc98bc8bbea as krtqozmx aa76b8a7 b1 + Rebased 2 commits onto duplicated commits + Working copy now at: nmzmmopx 0ad9462c d2 | d2 + Parent commit : knltnxnu a276dada a1 + Parent commit : krtqozmx aa76b8a7 b1 + Added 2 files, modified 0 files, removed 1 files + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 0ad9462c535b d2 + ├─╮ + │ │ ○ 16341f32c83b c2 + ╭─┬─╯ + │ ○ aa76b8a78db1 b1 + │ ├─╮ + ○ │ │ a276dadabfc1 a1 + ╰─┬─╮ + │ ○ 0f21c5e185c5 d1 + ○ │ b27346e9a9bd c1 + ├─╯ + ○ │ 7b44470918f4 b2 + ○ │ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship after + // multiple commits including an ancestor of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "b1", "--after", "a1", "--after", "c1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as wxzmtyol ccda812e a3 + Duplicated dcc98bc8bbea as musouqkq 560e532e b1 + Rebased 4 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ c1d222b0e288 c2 + ├─╮ + │ │ ○ 0a31f366f5a2 a4 + │ │ ○ 06750de0d803 a3 + │ │ ○ 031778a0e9f3 a2 + ╭─┬─╯ + │ ○ 560e532ebd75 b1 + │ ├─╮ + ○ │ │ ccda812e23c4 a3 + ╰─┬─╮ + │ ○ b27346e9a9bd c1 + ○ │ 9e85a474f005 a1 + ├─╯ + @ │ 0cdd923e993a d2 + ○ │ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship after + // multiple commits including a descendant of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--after", "a3", "--after", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as quyylypw b6a5e31d a1 + Duplicated dcc98bc8bbea as prukwozq dfe5dcad b1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 2db9fa035611 a4 + ├─╮ + │ ○ dfe5dcad355b b1 + │ ├─╮ + ○ │ │ b6a5e31daed5 a1 + ╰─┬─╮ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ○ │ 17072aa2b823 a3 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + @ │ 0cdd923e993a d2 + ○ │ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship after a single + // commit without a direct relationship. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a3", "--after", "c2"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as vvvtksvt 940b5139 a1 + Duplicated 17072aa2b823 as yvrnrpnw 9d985606 a3 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 9d9856065046 a3 + ○ 940b51398e5d a1 + ○ 09560d60cac4 c2 + ○ b27346e9a9bd c1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship after a single + // ancestor commit. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a2", "a3", "--after", "a1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Warning: Duplicating commit 47df67757a64 as an ancestor of itself + Duplicated 47df67757a64 as sukptuzs 4324d289 a2 + Duplicated 17072aa2b823 as rxnrppxl 47586b09 a3 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 2174f54d55a9 a4 + ○ 0224bfb4fc3d a3 + ○ 22d3bdc60967 a2 + ○ 47586b09a555 a3 + ○ 4324d289e62c a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship after a single + // descendant commit. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a2", "--after", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 47df67757a64 as a descendant of itself + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as rwkyzntp 08e917fe a1 + Duplicated 47df67757a64 as nqtyztop a80a88f5 a2 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ d1f47b881c72 a4 + ○ a80a88f5c6d6 a2 + ○ 08e917fe904c a1 + ○ 17072aa2b823 a3 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship after multiple + // commits without a direct relationship to the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a3", "--after", "c2", "--after", "d2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as nwmqwkzz 3d3385e3 a1 + Duplicated 17072aa2b823 as uwrrnrtx 3404101d a3 + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 3404101d5854 a3 + ○ 3d3385e379be a1 + ├─╮ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ○ │ 09560d60cac4 c2 + ○ │ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship after multiple + // commits including an ancestor of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "a4", "--after", "a2", "--after", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 196bc1f0efc1 as an ancestor of itself + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as wunttkrp 9d8de4c3 a3 + Duplicated 196bc1f0efc1 as puxpuzrm 71d9b4a4 a4 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ fc18e2f00060 a4 + ○ bc2303a7d63e a3 + ○ 71d9b4a48273 a4 + ○ 9d8de4c3ad3e a3 + ├─╮ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship after multiple + // commits including a descendant of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a2", "--after", "a3", "--after", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 47df67757a64 as a descendant of itself + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as zwvplpop cc0bfcbe a1 + Duplicated 47df67757a64 as znsksvls 0b619bbb a2 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 5006826a3086 a4 + ○ 0b619bbbe823 a2 + ○ cc0bfcbe97fe a1 + ├─╮ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ○ │ 17072aa2b823 a3 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Should error if a loop will be created. + let stderr = test_env.jj_cmd_failure( + &repo_path, + &["duplicate", "a1", "--after", "b1", "--after", "b2"], + ); + insta::assert_snapshot!(stderr, @r#" + Error: Refusing to create a loop: commit 7b44470918f4 would be both an ancestor and a descendant of the duplicated commits + "#); +} + +#[test] +fn test_duplicate_insert_before() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + create_commit(&test_env, &repo_path, "a1", &[]); + create_commit(&test_env, &repo_path, "a2", &["a1"]); + create_commit(&test_env, &repo_path, "a3", &["a2"]); + create_commit(&test_env, &repo_path, "a4", &["a3"]); + create_commit(&test_env, &repo_path, "b1", &[]); + create_commit(&test_env, &repo_path, "b2", &["b1"]); + create_commit(&test_env, &repo_path, "c1", &[]); + create_commit(&test_env, &repo_path, "c2", &["c1"]); + create_commit(&test_env, &repo_path, "d1", &[]); + create_commit(&test_env, &repo_path, "d2", &["d1"]); + let setup_opid = test_env.current_operation_id(&repo_path); + + // Test the setup + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 0cdd923e993a d2 + ○ 0f21c5e185c5 d1 + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + + // Duplicate a single commit before a single commit with no direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "--before", "b2"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as pzsxstzt b34eead0 a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ a384ab7ad1f6 b2 + ○ b34eead0fdf5 a1 + ○ dcc98bc8bbea b1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit before a single ancestor commit. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a3", "--before", "a1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as qmkrwlvp a982be78 a3 + Rebased 4 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 09981b821640 a4 + ○ 7f96a38d7b7b a3 + ○ d37b384f7ce9 a2 + ○ 4a0df1f03819 a1 + ○ a982be787d28 a3 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit before a single descendant commit. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "--before", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as qwyusntz 2b066074 a1 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 34812a9db795 a4 + ○ b42fc445deeb a3 + ○ 2b0660740e57 a1 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit before multiple commits with no direct + // relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--before", "b2", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as soqnvnyz 671da6dc a1 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 35ccc31b58bd c2 + │ ○ 7951d1641b4b b2 + ├─╯ + ○ 671da6dc2d2e a1 + ├─╮ + │ ○ b27346e9a9bd c1 + ○ │ dcc98bc8bbea b1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit before multiple commits including an ancestor. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "--before", "a2", "--before", "b2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as nsrwusvy 851a34a3 a3 + Rebased 4 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 3a9373464406 b2 + │ ○ 8774e5674831 a4 + │ ○ f3d3a1617059 a3 + │ ○ f207ecb81650 a2 + ├─╯ + ○ 851a34a36354 a3 + ├─╮ + │ ○ dcc98bc8bbea b1 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit before multiple commits including a descendant. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--before", "a3", "--before", "b2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as xpnwykqz af64c5e4 a1 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ f9f4cbe12efc b2 + │ ○ e8057839c645 a4 + │ ○ aa3ce5a43997 a3 + ├─╯ + ○ af64c5e44fc7 a1 + ├─╮ + │ ○ dcc98bc8bbea b1 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship before a + // single commit without a direct relationship. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "b1", "--before", "c1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as sryyqqkq fa625d74 a1 + Duplicated dcc98bc8bbea as pxnqtknr 2233b9a8 b1 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ cf7c4c4cc8bc c2 + ○ 6412acdac711 c1 + ├─╮ + │ ○ 2233b9a87d86 b1 + ○ │ fa625d74e0ae a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship before a + // single commit which is an ancestor of one of the duplicated commits. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a3", "b1", "--before", "a2"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as pyoswmwk 0a102776 a3 + Duplicated dcc98bc8bbea as yqnpwwmq 529ab44a b1 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ eb4ddce3bfef a4 + ○ b0b76f7bedf8 a3 + ○ b5fdef30de16 a2 + ├─╮ + │ ○ 529ab44a81ed b1 + ○ │ 0a1027765fdd a3 + ├─╯ + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship before a + // single commit which is a descendant of one of the duplicated commits. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "b1", "--before", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as tpmlxquz 7502d241 a1 + Duplicated dcc98bc8bbea as uukzylyy 63ba24cf b1 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 84d66cf1a667 a4 + ○ 733e5aa5ee67 a3 + ├─╮ + │ ○ 63ba24cf71df b1 + ○ │ 7502d2419a00 a1 + ├─╯ + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship before + // multiple commits without a direct relationship to the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--before", "c1", "--before", "d1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as knltnxnu 056a0cb3 a1 + Duplicated dcc98bc8bbea as krtqozmx fb68a539 b1 + Rebased 4 commits onto duplicated commits + Working copy now at: nmzmmopx 89f9b379 d2 | d2 + Parent commit : xznxytkn 771d0e16 d1 | d1 + Added 2 files, modified 0 files, removed 0 files + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 89f9b37923a9 d2 + ○ 771d0e16b40c d1 + ├─╮ + │ │ ○ 7e7653d32cf1 c2 + │ │ ○ a83b8a44f3fc c1 + ╭─┬─╯ + │ ○ fb68a539aea7 b1 + ○ │ 056a0cb391f8 a1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship before + // multiple commits including an ancestor of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "b1", "--before", "a1", "--before", "c1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as wxzmtyol 4aef0293 a3 + Duplicated dcc98bc8bbea as musouqkq 4748cf83 b1 + Rebased 6 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ a86830bda155 c2 + ○ dfa992eb0c5b c1 + ├─╮ + │ │ ○ 2a975bb6fb8d a4 + │ │ ○ bd65348afea2 a3 + │ │ ○ 5aaf2e32fe6e a2 + │ │ ○ c1841f6cb78b a1 + ╭─┬─╯ + │ ○ 4748cf83e26e b1 + ○ │ 4aef02939dcb a3 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship before + // multiple commits including a descendant of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--before", "a3", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as quyylypw 024440c4 a1 + Duplicated dcc98bc8bbea as prukwozq 8175fcec b1 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 7a485e3977a8 c2 + ├─╮ + │ │ ○ e5464cd6273d a4 + │ │ ○ e7bb732c469e a3 + ╭─┬─╯ + │ ○ 8175fcec2ded b1 + │ ├─╮ + ○ │ │ 024440c4a5da a1 + ╰─┬─╮ + │ ○ b27346e9a9bd c1 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + @ │ 0cdd923e993a d2 + ○ │ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship before a single + // commit without a direct relationship. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a3", "--before", "c2"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as vvvtksvt ad5a3d82 a1 + Duplicated 17072aa2b823 as yvrnrpnw 441a2568 a3 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 756972984dac c2 + ○ 441a25683840 a3 + ○ ad5a3d824060 a1 + ○ b27346e9a9bd c1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship before a single + // ancestor commit. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a3", "--before", "a1"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Warning: Duplicating commit 9e85a474f005 as an ancestor of itself + Duplicated 9e85a474f005 as sukptuzs ad0234a3 a1 + Duplicated 17072aa2b823 as rxnrppxl b72e2eaa a3 + Rebased 4 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ de1a87f140d9 a4 + ○ 3b405d96fbfb a3 + ○ 41677a1f0572 a2 + ○ 00c6a7cebcdb a1 + ○ b72e2eaa3f7f a3 + ○ ad0234a34661 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship before a single + // descendant commit. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["duplicate", "a1", "a2", "--before", "a3"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 47df67757a64 as a descendant of itself + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as rwkyzntp 2fdd3c3d a1 + Duplicated 47df67757a64 as nqtyztop bddcdcd1 a2 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 13038f9969fa a4 + ○ 327c3bc13b75 a3 + ○ bddcdcd1ef61 a2 + ○ 2fdd3c3dabfc a1 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship before multiple + // commits without a direct relationship to the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a3", "--before", "c2", "--before", "d2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as nwmqwkzz aa5bda17 a1 + Duplicated 17072aa2b823 as uwrrnrtx 7a739397 a3 + Rebased 2 commits onto duplicated commits + Working copy now at: nmzmmopx ba3800be d2 | d2 + Parent commit : uwrrnrtx 7a739397 a3 + Added 3 files, modified 0 files, removed 1 files + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ ba3800bec255 d2 + │ ○ 6052b049d679 c2 + ├─╯ + ○ 7a73939747a8 a3 + ○ aa5bda171182 a1 + ├─╮ + │ ○ 0f21c5e185c5 d1 + ○ │ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship before multiple + // commits including an ancestor of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "a4", "--before", "a2", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 196bc1f0efc1 as an ancestor of itself + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as wunttkrp c7b7f78f a3 + Duplicated 196bc1f0efc1 as puxpuzrm 196c76cf a4 + Rebased 4 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ d7ea487131da c2 + │ ○ f8d49609e8d8 a4 + │ ○ e3d75d821d33 a3 + │ ○ 23d8d39dd2d1 a2 + ├─╯ + ○ 196c76cf739f a4 + ○ c7b7f78f8924 a3 + ├─╮ + │ ○ b27346e9a9bd c1 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship before multiple + // commits including a descendant of one of the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a2", "--before", "a3", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 47df67757a64 as a descendant of itself + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as zwvplpop 26d71f93 a1 + Duplicated 47df67757a64 as znsksvls 37c5c955 a2 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ d269d405ab74 c2 + │ ○ 175de6d6b816 a4 + │ ○ cdd9df354b86 a3 + ├─╯ + ○ 37c5c955a90a a2 + ○ 26d71f93323b a1 + ├─╮ + │ ○ b27346e9a9bd c1 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Should error if a loop will be created. + let stderr = test_env.jj_cmd_failure( + &repo_path, + &["duplicate", "a1", "--before", "b1", "--before", "b2"], + ); + insta::assert_snapshot!(stderr, @r#" + Error: Refusing to create a loop: commit dcc98bc8bbea would be both an ancestor and a descendant of the duplicated commits + "#); +} + +#[test] +fn test_duplicate_insert_after_before() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + create_commit(&test_env, &repo_path, "a1", &[]); + create_commit(&test_env, &repo_path, "a2", &["a1"]); + create_commit(&test_env, &repo_path, "a3", &["a2"]); + create_commit(&test_env, &repo_path, "a4", &["a3"]); + create_commit(&test_env, &repo_path, "b1", &[]); + create_commit(&test_env, &repo_path, "b2", &["b1"]); + create_commit(&test_env, &repo_path, "c1", &[]); + create_commit(&test_env, &repo_path, "c2", &["c1"]); + create_commit(&test_env, &repo_path, "d1", &[]); + create_commit(&test_env, &repo_path, "d2", &["d1"]); + let setup_opid = test_env.current_operation_id(&repo_path); + + // Test the setup + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 0cdd923e993a d2 + ○ 0f21c5e185c5 d1 + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + + // Duplicate a single commit in between commits with no direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--before", "b2", "--after", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as pzsxstzt d5ebd2c8 a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 20cc68b3be82 b2 + ├─╮ + │ ○ d5ebd2c814fb a1 + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ○ │ dcc98bc8bbea b1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit in between ancestor commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "--before", "a2", "--after", "a1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as qmkrwlvp c167d08f a3 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 8746d17a44cb a4 + ○ 15a695f5bf13 a3 + ○ 73e26c9e22e7 a2 + ○ c167d08f8d9f a3 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit in between an ancestor commit and a commit with no + // direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "--before", "a2", "--after", "b2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as qwyusntz 0481e43c a3 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 68632a4645b3 a4 + ○ 61736eaab064 a3 + ○ b8822ec79abf a2 + ├─╮ + │ ○ 0481e43c0ba7 a3 + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit in between descendant commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--after", "a3", "--before", "a4"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as soqnvnyz 981c26cf a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 53de53f5df1d a4 + ○ 981c26cf1d8c a1 + ○ 17072aa2b823 a3 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit in between a descendant commit and a commit with no + // direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "--after", "a3", "--before", "b2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as nsrwusvy e4ec1bed a1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 0ec3be87fae7 b2 + ├─╮ + │ ○ e4ec1bed0e7c a1 + ○ │ dcc98bc8bbea b1 + │ │ @ 0cdd923e993a d2 + │ │ ○ 0f21c5e185c5 d1 + ├───╯ + │ │ ○ 09560d60cac4 c2 + │ │ ○ b27346e9a9bd c1 + ├───╯ + │ │ ○ 196bc1f0efc1 a4 + │ ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate a single commit in between an ancestor commit and a descendant + // commit. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a2", "--after", "a1", "--before", "a4"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 47df67757a64 as xpnwykqz 54cc0161 a2 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ b08d6199fab9 a4 + ├─╮ + │ ○ 54cc0161a5db a2 + ○ │ 17072aa2b823 a3 + ○ │ 47df67757a64 a2 + ├─╯ + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship between + // commits without a direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--after", "c1", "--before", "d2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as sryyqqkq d3dda93b a1 + Duplicated dcc98bc8bbea as pxnqtknr 21b26c06 b1 + Rebased 1 commits onto duplicated commits + Working copy now at: nmzmmopx 16aa6cc4 d2 | d2 + Parent commit : xznxytkn 0f21c5e1 d1 | d1 + Parent commit : sryyqqkq d3dda93b a1 + Parent commit : pxnqtknr 21b26c06 b1 + Added 2 files, modified 0 files, removed 0 files + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 16aa6cc4b9ff d2 + ├─┬─╮ + │ │ ○ 21b26c06639f b1 + │ ○ │ d3dda93b8e6f a1 + │ ├─╯ + ○ │ 0f21c5e185c5 d1 + │ │ ○ 09560d60cac4 c2 + │ ├─╯ + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship between a + // commit which is an ancestor of one of the duplicated commits and a commit + // with no direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "b1", "--after", "a2", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 17072aa2b823 as pyoswmwk 0d11d466 a3 + Duplicated dcc98bc8bbea as yqnpwwmq f18498f2 b1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ da87b56a17e4 c2 + ├─┬─╮ + │ │ ○ f18498f24737 b1 + │ ○ │ 0d11d4667aa9 a3 + │ ├─╯ + ○ │ b27346e9a9bd c1 + │ │ @ 0cdd923e993a d2 + │ │ ○ 0f21c5e185c5 d1 + ├───╯ + │ │ ○ 7b44470918f4 b2 + │ │ ○ dcc98bc8bbea b1 + ├───╯ + │ │ ○ 196bc1f0efc1 a4 + │ │ ○ 17072aa2b823 a3 + │ ├─╯ + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship between a + // commit which is a descendant of one of the duplicated commits and a + // commit with no direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--after", "a3", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as tpmlxquz b7458ffe a1 + Duplicated dcc98bc8bbea as uukzylyy 7366036f b1 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 61237f8ed16f c2 + ├─┬─╮ + │ │ ○ 7366036f148d b1 + │ ○ │ b7458ffedb08 a1 + │ ├─╯ + ○ │ b27346e9a9bd c1 + │ │ @ 0cdd923e993a d2 + │ │ ○ 0f21c5e185c5 d1 + ├───╯ + │ │ ○ 7b44470918f4 b2 + │ │ ○ dcc98bc8bbea b1 + ├───╯ + │ │ ○ 196bc1f0efc1 a4 + │ ├─╯ + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits without a direct ancestry relationship between + // commits without a direct relationship to the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "b1", "--after", "c1", "--before", "d2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as knltnxnu 8d6944d2 a1 + Duplicated dcc98bc8bbea as krtqozmx b75e34da b1 + Rebased 1 commits onto duplicated commits + Working copy now at: nmzmmopx 559d8248 d2 | d2 + Parent commit : xznxytkn 0f21c5e1 d1 | d1 + Parent commit : knltnxnu 8d6944d2 a1 + Parent commit : krtqozmx b75e34da b1 + Added 2 files, modified 0 files, removed 0 files + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 559d82485798 d2 + ├─┬─╮ + │ │ ○ b75e34daf1e8 b1 + │ ○ │ 8d6944d2344d a1 + │ ├─╯ + ○ │ 0f21c5e185c5 d1 + │ │ ○ 09560d60cac4 c2 + │ ├─╯ + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship between + // commits without a direct relationship to the duplicated commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a3", "--after", "c1", "--before", "d2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as wxzmtyol db340447 a1 + Duplicated 17072aa2b823 as musouqkq 73e5fec0 a3 + Rebased 1 commits onto duplicated commits + Working copy now at: nmzmmopx dfbf0b36 d2 | d2 + Parent commit : xznxytkn 0f21c5e1 d1 | d1 + Parent commit : musouqkq 73e5fec0 a3 + Added 3 files, modified 0 files, removed 0 files + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ dfbf0b367dee d2 + ├─╮ + │ ○ 73e5fec0d840 a3 + │ ○ db340447c78a a1 + ○ │ 0f21c5e185c5 d1 + │ │ ○ 09560d60cac4 c2 + │ ├─╯ + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + │ ○ 196bc1f0efc1 a4 + │ ○ 17072aa2b823 a3 + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship between a commit + // which is an ancestor of one of the duplicated commits and a commit + // without a direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "a4", "--after", "a2", "--before", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 17072aa2b823 as quyylypw d4d3c907 a3 + Duplicated 196bc1f0efc1 as prukwozq 96798f1b a4 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 267f3c6f05a2 c2 + ├─╮ + │ ○ 96798f1b59fc a4 + │ ○ d4d3c9073a3b a3 + ○ │ b27346e9a9bd c1 + │ │ @ 0cdd923e993a d2 + │ │ ○ 0f21c5e185c5 d1 + ├───╯ + │ │ ○ 7b44470918f4 b2 + │ │ ○ dcc98bc8bbea b1 + ├───╯ + │ │ ○ 196bc1f0efc1 a4 + │ │ ○ 17072aa2b823 a3 + │ ├─╯ + │ ○ 47df67757a64 a2 + │ ○ 9e85a474f005 a1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship between a commit + // which is a a descendant of one of the duplicated commits and a commit + // with no direct relationship. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a2", "--before", "a3", "--after", "c2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 9e85a474f005 as vvvtksvt 940b5139 a1 + Duplicated 47df67757a64 as yvrnrpnw 72eb571c a2 + Rebased 2 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ b5ab4b26d9a2 a4 + ○ 64f9306ab0d0 a3 + ├─╮ + │ ○ 72eb571caee0 a2 + │ ○ 940b51398e5d a1 + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ○ │ 47df67757a64 a2 + ○ │ 9e85a474f005 a1 + ├─╯ + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship between descendant + // commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a3", "a4", "--after", "a1", "--before", "a2"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 196bc1f0efc1 as an ancestor of itself + Warning: Duplicating commit 17072aa2b823 as an ancestor of itself + Duplicated 17072aa2b823 as sukptuzs 54dec05c a3 + Duplicated 196bc1f0efc1 as rxnrppxl 53c4e5dd a4 + Rebased 3 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 7668841ec9b9 a4 + ○ 223fd997dec0 a3 + ○ 9750bf965aff a2 + ○ 53c4e5ddca56 a4 + ○ 54dec05c42f1 a3 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship between ancestor + // commits. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a1", "a2", "--after", "a3", "--before", "a4"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Warning: Duplicating commit 47df67757a64 as a descendant of itself + Warning: Duplicating commit 9e85a474f005 as a descendant of itself + Duplicated 9e85a474f005 as rwkyzntp 08e917fe a1 + Duplicated 47df67757a64 as nqtyztop a80a88f5 a2 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ d1f47b881c72 a4 + ○ a80a88f5c6d6 a2 + ○ 08e917fe904c a1 + ○ 17072aa2b823 a3 + ○ 47df67757a64 a2 + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Duplicate multiple commits with an ancestry relationship between an ancestor + // commit and a descendant commit. + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["duplicate", "a2", "a3", "--after", "a1", "--before", "a4"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r#" + Duplicated 47df67757a64 as nwmqwkzz 8517eaa7 a2 + Duplicated 17072aa2b823 as uwrrnrtx 3ce18231 a3 + Rebased 1 commits onto duplicated commits + "#); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + ○ 0855137fa398 a4 + ├─╮ + │ ○ 3ce182317a5b a3 + │ ○ 8517eaa73536 a2 + ○ │ 17072aa2b823 a3 + ○ │ 47df67757a64 a2 + ├─╯ + ○ 9e85a474f005 a1 + │ @ 0cdd923e993a d2 + │ ○ 0f21c5e185c5 d1 + ├─╯ + │ ○ 09560d60cac4 c2 + │ ○ b27346e9a9bd c1 + ├─╯ + │ ○ 7b44470918f4 b2 + │ ○ dcc98bc8bbea b1 + ├─╯ + ◆ 000000000000 + "#); + test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); + + // Should error if a loop will be created. + let stderr = test_env.jj_cmd_failure( + &repo_path, + &["duplicate", "a1", "--after", "b2", "--before", "b1"], + ); + insta::assert_snapshot!(stderr, @r#" + Error: Refusing to create a loop: commit 7b44470918f4 would be both an ancestor and a descendant of the duplicated commits + "#); +} + // https://github.com/martinvonz/jj/issues/1050 #[test] fn test_undo_after_duplicate() { diff --git a/docs/git-comparison.md b/docs/git-comparison.md index d110feb9d7..4923a9bb52 100644 --- a/docs/git-comparison.md +++ b/docs/git-comparison.md @@ -320,8 +320,7 @@ parent. Create a copy of a commit on top of another commit - jj duplicate <source>; jj rebase -r <duplicate commit> -d <destination> - (there's no single command for it yet) + jj duplicate <source>; -d <destination> git co <destination>; git cherry-pick <source>