diff --git a/CHANGELOG.md b/CHANGELOG.md index 029569e05d..6a6f02f149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Added the config setting `snapshot.auto-update-stale` for automatically running `jj workspace update-stale` when applicable. +* `jj duplicate` now accepts `--destination`, `--insert-after` and + `--insert-before` options to customize the location of the duplicated + revisions. + ### Fixed bugs ## [0.23.0] - 2024-11-06 diff --git a/cli/src/commands/duplicate.rs b/cli/src/commands/duplicate.rs index c229ca0c46..96f77815c6 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 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::ResolvedRevsetExpression; +use jj_lib::revset::RevsetExpression; +use jj_lib::rewrite::duplicate_commits; +use jj_lib::rewrite::duplicate_commits_onto_parents; +use jj_lib::rewrite::DuplicateCommitsStats; use tracing::instrument; use crate::cli_util::short_commit_hash; @@ -28,7 +34,19 @@ use crate::command_error::user_error; use crate::command_error::CommandError; use crate::ui::Ui; -/// Create a new change with the same content as an existing one +/// Create new changes with the same content as existing ones +/// +/// When none of the `--destination`, `--insert-after`, or `--insert-before` +/// arguments are provided, commits will be duplicated onto their existing +/// parents or onto other newly duplicated commits. +/// +/// When any of the `--destination`, `--insert-after`, or `--insert-before` +/// arguments are provided, the roots of the specified commits will be +/// duplicated onto the destination indicated by the arguments. Other specified +/// commits will be duplicated onto these newly duplicated commits. If the +/// `--insert-after` or `--insert-before` arguments are provided, the new +/// children indicated by the arguments will be rebased onto the heads of the +/// specified commits. #[derive(clap::Args, Clone, Debug)] pub(crate) struct DuplicateArgs { /// The revision(s) to duplicate @@ -37,6 +55,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 +97,167 @@ 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(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 DuplicateCommitsStats { + duplicated_commits, + num_rebased, + } = if args.destination.is_empty() + && args.insert_after.is_empty() + && args.insert_before.is_empty() + { + duplicate_commits_onto_parents(command.settings(), tx.repo_mut(), &to_duplicate)? + } else { + duplicate_commits( + command.settings(), + tx.repo_mut(), + &to_duplicate, + &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(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 c8850015a2..aba658a3a3 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1,7 +1,6 @@ --- source: cli/tests/test_generate_md_cli_help.rs description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md." -snapshot_kind: text --- @@ -130,7 +129,7 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor * `describe` — Update the change description or other metadata [aliases: desc] * `diff` — Compare file contents between two revisions * `diffedit` — Touch up the content changes in a revision with a diff editor -* `duplicate` — Create a new change with the same content as an existing one +* `duplicate` — Create new changes with the same content as existing ones * `edit` — Sets the specified revision as the working-copy revision * `evolog` — Show how a change has evolved over time * `file` — File operations @@ -745,9 +744,13 @@ See `jj restore` if you want to move entire files from one revision to another. ## `jj duplicate` -Create a new change with the same content as an existing one +Create new changes with the same content as existing ones -**Usage:** `jj duplicate [REVISIONS]...` +When none of the `--destination`, `--insert-after`, or `--insert-before` arguments are provided, commits will be duplicated onto their existing parents or onto other newly duplicated commits. + +When any of the `--destination`, `--insert-after`, or `--insert-before` arguments are provided, the roots of the specified commits will be duplicated onto the destination indicated by the arguments. Other specified commits will be duplicated onto these newly duplicated commits. If the `--insert-after` or `--insert-before` arguments are provided, the new children indicated by the arguments will be rebased onto the heads of the specified commits. + +**Usage:** `jj duplicate [OPTIONS] [REVISIONS]...` ###### **Arguments:** @@ -755,6 +758,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 a9770eb2d7..a5e3601b56 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 6b30117293..43e280a2f2 100644 --- a/docs/git-comparison.md +++ b/docs/git-comparison.md @@ -324,8 +324,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>