diff --git a/CHANGELOG.md b/CHANGELOG.md index e046d966c..be6a7b45b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj new --insert-before` inserts the new commit between the target commit and its parents. +* `jj new --insert-after` inserts the new commit between the target commit and + its children. + ### Fixed bugs * When sharing the working copy with a Git repo, we used to forget to export diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b9c7b6af1..6b67b7c4d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -446,6 +446,7 @@ struct EditArgs { /// For more information, see /// https://github.com/martinvonz/jj/blob/main/docs/working-copy.md. #[derive(clap::Args, Clone, Debug)] +#[command(group(ArgGroup::new("order").args(&["insert_after", "insert_before"])))] struct NewArgs { /// Parent(s) of the new change #[arg(default_value = "@")] @@ -459,6 +460,9 @@ struct NewArgs { /// Allow revsets expanding to multiple commits in a single argument #[arg(long, short = 'L')] allow_large_revsets: bool, + /// Insert the new change between the target commit(s) and their children + #[arg(long, short = 'A', visible_alias = "after")] + insert_after: bool, /// Insert the new change between the target commit(s) and their parents #[arg(long, short = 'B', visible_alias = "before")] insert_before: bool, @@ -2097,9 +2101,45 @@ fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &NewArgs) -> Result<(), C let merged_tree = merge_commit_trees(tx.base_repo().as_repo_ref(), &target_commits); new_commit = tx .mut_repo() - .new_commit(command.settings(), target_ids, merged_tree.id().clone()) + .new_commit( + command.settings(), + target_ids.clone(), + merged_tree.id().clone(), + ) .set_description(&args.message) .write()?; + if args.insert_after { + // Each child of the targets will be rebased: its set of parents will be updated + // so that the targets are replaced by the new commit. + let old_parents = RevsetExpression::commits(target_ids); + // Exclude children that are ancestors of the new commit + let to_rebase = old_parents.children().minus(&old_parents.ancestors()); + let commits_to_rebase: Vec = tx + .base_workspace_helper() + .evaluate_revset(&to_rebase)? + .iter() + .commits(tx.base_repo().store()) + .try_collect()?; + num_rebased = commits_to_rebase.len(); + for child_commit in commits_to_rebase { + let commit_parents = + RevsetExpression::commits(child_commit.parent_ids().to_owned()); + let new_parents = commit_parents.minus(&old_parents); + let mut new_parent_commits: Vec = tx + .base_workspace_helper() + .evaluate_revset(&new_parents)? + .iter() + .commits(tx.base_repo().store()) + .try_collect()?; + new_parent_commits.push(new_commit.clone()); + rebase_commit( + command.settings(), + tx.mut_repo(), + &child_commit, + &new_parent_commits, + )?; + } + } } num_rebased += tx.mut_repo().rebase_descendants(command.settings())?; if num_rebased > 0 { diff --git a/tests/test_new_command.rs b/tests/test_new_command.rs index b1974dd0b..2b95e7101 100644 --- a/tests/test_new_command.rs +++ b/tests/test_new_command.rs @@ -106,6 +106,116 @@ fn test_new_merge() { "###); } +#[test] +fn test_new_insert_after() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + setup_before_insertion(&test_env, &repo_path); + insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" + @ F + |\ + o | E + | o D + |/ + | o C + | o B + | o A + |/ + o root + "###); + + let stdout = + test_env.jj_cmd_success(&repo_path, &["new", "--insert-after", "-m", "G", "B", "D"]); + insta::assert_snapshot!(stdout, @r###" + Rebased 2 descendant commits + Working copy now at: ca7c6481a8dd G + "###); + insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" + o C + | o F + | |\ + |/ / + @ | G + |\ \ + | | o E + o | | D + | |/ + |/| + | o B + | o A + |/ + o root + "###); + + let stdout = test_env.jj_cmd_success(&repo_path, &["new", "--insert-after", "-m", "H", "D"]); + insta::assert_snapshot!(stdout, @r###" + Rebased 3 descendant commits + Working copy now at: fcf8281b4135 H + "###); + insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" + o C + | o F + | |\ + |/ / + o | G + |\ \ + @ | | H + | | o E + o | | D + | |/ + |/| + | o B + | o A + |/ + o root + "###); +} + +#[test] +fn test_new_insert_after_children() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + setup_before_insertion(&test_env, &repo_path); + insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" + @ F + |\ + o | E + | o D + |/ + | o C + | o B + | o A + |/ + o root + "###); + + // Check that inserting G after A and C doesn't try to rebase B (which is + // initially a child of A) onto G as that would create a cycle since B is + // a parent of C which is a parent G. + let stdout = + test_env.jj_cmd_success(&repo_path, &["new", "--insert-after", "-m", "G", "A", "C"]); + insta::assert_snapshot!(stdout, @r###" + Working copy now at: b48d4d73a39c G + "###); + insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" + @ G + |\ + | | o F + | | |\ + | | o | E + | | | o D + | | |/ + o | | C + o | | B + |/ / + o | A + |/ + o root + "###); +} + #[test] fn test_new_insert_before() { let test_env = TestEnvironment::default();