diff --git a/cli/src/commands/parallelize.rs b/cli/src/commands/parallelize.rs index 9e311d327c..083a36e030 100644 --- a/cli/src/commands/parallelize.rs +++ b/cli/src/commands/parallelize.rs @@ -12,21 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::HashSet; -use std::io::Write; -use std::rc::Rc; +use std::collections::HashMap; use indexmap::IndexSet; use itertools::Itertools; use jj_lib::backend::CommitId; use jj_lib::commit::{Commit, CommitIteratorExt}; -use jj_lib::repo::Repo; -use jj_lib::revset::{RevsetExpression, RevsetIteratorExt}; use tracing::instrument; use crate::cli_util::{CommandHelper, RevisionArg}; -use crate::command_error::{user_error, CommandError}; -use crate::commands::rebase::rebase_descendants; +use crate::command_error::CommandError; use crate::ui::Ui; /// Parallelize revisions by making them siblings @@ -42,18 +37,18 @@ use crate::ui::Ui; /// 0 /// ``` /// -/// Each of the target revisions is rebased onto the parents of the root(s) of -/// the target revset (not to be confused with the repo root). The children of -/// the head(s) of the target revset are rebased onto the target revisions. +/// The command effectively says "these revisions are actually independent", +/// meaning that they should no longer be ancestors/descendants of each other. +/// However, revisions outside the set that were previously ancestors of a +/// revision in the set will remain ancestors of it. For example, revision 0 +/// above remains an ancestor of both 1 and 2. Similarly, +/// revisions outside the set that were previously descendants of a revision +/// in the set will remain descendants of it. For example, revision 3 above +/// remains a descendant of both 1 and 2. /// -/// The target revset is the union of the `revisions` arguments and must satisfy -/// several conditions, otherwise the command will fail. -/// -/// 1. The heads of the target revset must have either the same children as the -/// other heads or none. -/// 2. The roots of the target revset have the same parents. -/// 3. The parents of all target revisions except the roots must also be -/// parallelized. This means that the target revisions must be connected. +/// Therefore, `jj parallelize '1 | 3'` is a no-op. That's because 2, which is +/// not in the target set, was a descendant of 1 before, so it remains a +/// descendant, and it was an ancestor of 3 before, so it remains an ancestor. #[derive(clap::Args, Clone, Debug)] #[command(verbatim_doc_comment)] pub(crate) struct ParallelizeArgs { @@ -69,189 +64,77 @@ pub(crate) fn cmd_parallelize( ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; // The target commits are the commits being parallelized. They are ordered - // here with parents before children. + // here with children before parents. let target_commits: Vec = workspace_command .parse_union_revsets(&args.revisions)? .evaluate_to_commits()? .try_collect()?; - if target_commits.len() < 2 { - writeln!(ui.status(), "Nothing changed.")?; - return Ok(()); - } workspace_command.check_rewritable(target_commits.iter().ids())?; let mut tx = workspace_command.start_transaction(); - let target_revset = - RevsetExpression::commits(target_commits.iter().ids().cloned().collect_vec()); - let new_parents = - check_preconditions_and_get_new_parents(&target_revset, &target_commits, tx.repo())?; - - // Rebase the non-target children of each target commit onto its new - // parents. A child which had a target commit as an ancestor before - // parallelize ran will have the target commit as a parent afterward. - for target_commit in target_commits.iter() { - // Children of the target commit, excluding other target commits. - let children: Vec = RevsetExpression::commit(target_commit.id().clone()) - .children() - .minus(&target_revset) - .evaluate_programmatic(tx.repo())? - .iter() - .commits(tx.repo().store()) - .try_collect()?; - // These parents are shared by all children of the target commit and - // include the target commit itself plus any of its ancestors which are - // being parallelized. - let common_parents: IndexSet = RevsetExpression::commit(target_commit.id().clone()) - .ancestors() - .intersection(&target_revset) - .evaluate_programmatic(tx.repo())? - .iter() - .commits(tx.repo().store()) - .try_collect()?; - for child in children { - let mut new_parents = common_parents.clone(); - new_parents.extend(child.parents().into_iter()); - rebase_descendants( - &mut tx, - command.settings(), - new_parents.into_iter().collect_vec(), - &[child], - Default::default(), - )?; - } - } - // Rebase the target commits onto the parents of the root commit. - // We already checked that the roots have the same parents, so we can just - // use the first one. - let target_commits = target_commits - .into_iter() - // We need to reverse the iterator so that when we rebase the target - // commits they will appear in the same relative order in `jj log` that - // they were in before being parallelized. After reversing, the commits - // are ordered with children before parents. - .rev() - .collect_vec(); - rebase_descendants( - &mut tx, - command.settings(), - new_parents, - &target_commits, - Default::default(), - )?; - - tx.finish(ui, format!("parallelize {} commits", target_commits.len())) -} - -/// Returns the new parents of the parallelized commits or an error if any of -/// the following preconditions are not met: -/// -/// 1. If the heads of the target revset must not have different children. -/// 2. The roots of the target revset must not have different parents. -/// 3. The parents of all target revisions except the roots must also be -/// parallelized. This means that the target revisions must be connected. -/// -/// The `target_revset` must evaluate to the commits in `target_commits` when -/// the provided `repo` is used. -fn check_preconditions_and_get_new_parents( - target_revset: &Rc, - target_commits: &[Commit], - repo: &dyn Repo, -) -> Result, CommandError> { - check_target_heads(target_revset, repo)?; - let target_roots = check_target_roots(target_revset, repo)?; - check_target_commits_are_connected(&target_roots, target_commits)?; - - // We already verified that the roots have the same parents, so we can just - // use the first root. - Ok(target_roots[0].parents()) -} - -/// Returns an error if the heads of the target revset have children which are -/// different. -fn check_target_heads( - target_revset: &Rc, - repo: &dyn Repo, -) -> Result<(), CommandError> { - let target_heads = target_revset - .heads() - .evaluate_programmatic(repo)? - .iter() - .sorted() - .collect_vec(); - if target_heads.len() == 1 { - return Ok(()); - } - let all_children: Vec = target_revset - .heads() - .children() - .evaluate_programmatic(repo)? - .iter() - .commits(repo.store()) - .try_collect()?; - // Every child must have every target head as a parent, otherwise it means - // that the target heads have different children. - for child in all_children { - let parents = child.parent_ids().iter().sorted(); - if !parents.eq(target_heads.iter()) { - return Err(user_error( - "All heads of the target revisions must have the same children.", - )); + // New parents for commits in the target set. Since commits in the set are now + // supposed to be independent, they inherit the parent's non-target parents, + // recursively. + let mut new_target_parents: HashMap> = HashMap::new(); + for commit in target_commits.iter().rev() { + let mut new_parents = vec![]; + for old_parent in commit.parent_ids() { + if let Some(grand_parents) = new_target_parents.get(old_parent) { + new_parents.extend_from_slice(grand_parents); + } else { + new_parents.push(old_parent.clone()); + } } + new_target_parents.insert(commit.id().clone(), new_parents); } - Ok(()) -} -/// Returns the roots of the target revset or an error if their parents are -/// different. -fn check_target_roots( - target_revset: &Rc, - repo: &dyn Repo, -) -> Result, CommandError> { - let target_roots: Vec = target_revset - .roots() - .evaluate_programmatic(repo)? - .iter() - .commits(repo.store()) - .try_collect()?; - let expected_parents = target_roots[0].parent_ids().iter().sorted().collect_vec(); - for root in target_roots[1..].iter() { - let root_parents = root.parent_ids().iter().sorted(); - if !root_parents.eq(expected_parents.iter().copied()) { - return Err(user_error( - "All roots of the target revisions must have the same parents.", - )); + // If a commit outside the target set has a commit in the target set as parent, + // then - after the transformation - it should also have that commit's + // parents as direct parents, if those commits are also in the target set. + let mut new_child_parents: HashMap> = HashMap::new(); + for commit in target_commits.iter().rev() { + let mut new_parents = IndexSet::new(); + for old_parent in commit.parent_ids() { + if let Some(parents) = new_child_parents.get(old_parent) { + new_parents.extend(parents.iter().cloned()); + } } + new_parents.insert(commit.id().clone()); + new_child_parents.insert(commit.id().clone(), new_parents); } - Ok(target_roots) -} -/// The target commits must be connected. The parents of every target commit -/// except the root commit must also be target commits. Returns an error if this -/// requirement is not met. -fn check_target_commits_are_connected( - target_roots: &[Commit], - target_commits: &[Commit], -) -> Result<(), CommandError> { - let target_commit_ids: HashSet = target_commits.iter().ids().cloned().collect(); - for target_commit in target_commits.iter() { - if target_roots.iter().ids().contains(target_commit.id()) { - continue; - } - for parent in target_commit.parent_ids() { - if !target_commit_ids.contains(parent) { - // We check this condition to return a more useful error to the user. - if target_commit.parent_ids().len() == 1 { - return Err(user_error( - "Cannot parallelize since the target revisions are not connected.", - )); + tx.mut_repo().transform_descendants( + command.settings(), + target_commits.iter().ids().cloned().collect_vec(), + |mut rewriter| { + // Commits in the target set do not depend on each other but they still depend + // on other parents + if let Some(new_parents) = new_target_parents.get(rewriter.old_commit().id()) { + rewriter.set_new_rewritten_parents(new_parents.clone()); + } else if rewriter + .old_commit() + .parent_ids() + .iter() + .any(|id| new_child_parents.contains_key(id)) + { + let mut new_parents = vec![]; + for parent in rewriter.old_commit().parent_ids() { + if let Some(parents) = new_child_parents.get(parent) { + new_parents.extend(parents.iter().cloned()); + } else { + new_parents.push(parent.clone()); + } } - return Err(user_error( - "Only the roots of the target revset are allowed to have parents which are \ - not being parallelized.", - )); + rewriter.set_new_rewritten_parents(new_parents); } - } - } - Ok(()) + if rewriter.parents_changed() { + let builder = rewriter.rebase(command.settings())?; + builder.write()?; + } + Ok(()) + }, + )?; + + tx.finish(ui, format!("parallelize {} commits", target_commits.len())) } diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index c3a50add63..4dd0ba62cb 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1369,18 +1369,18 @@ Running `jj parallelize 1::2` will transform the history like this: 0 ``` -Each of the target revisions is rebased onto the parents of the root(s) of -the target revset (not to be confused with the repo root). The children of -the head(s) of the target revset are rebased onto the target revisions. - -The target revset is the union of the `revisions` arguments and must satisfy -several conditions, otherwise the command will fail. - -1. The heads of the target revset must have either the same children as the - other heads or none. -2. The roots of the target revset have the same parents. -3. The parents of all target revisions except the roots must also be - parallelized. This means that the target revisions must be connected. +The command effectively says "these revisions are actually independent", +meaning that they should no longer be ancestors/descendants of each other. +However, revisions outside the set that were previously ancestors of a +revision in the set will remain ancestors of it. For example, revision 0 +above remains an ancestor of both 1 and 2. Similarly, +revisions outside the set that were previously descendants of a revision +in the set will remain descendants of it. For example, revision 3 above +remains a descendant of both 1 and 2. + +Therefore, `jj parallelize '1 | 3'` is a no-op. That's because 2, which is +not in the target set, was a descendant of 1 before, so it remains a +descendant, and it was an ancestor of 3 before, so it remains an ancestor. **Usage:** `jj parallelize [REVISIONS]...` diff --git a/cli/tests/test_parallelize_command.rs b/cli/tests/test_parallelize_command.rs index 1f941f761e..ea43756cfb 100644 --- a/cli/tests/test_parallelize_command.rs +++ b/cli/tests/test_parallelize_command.rs @@ -27,29 +27,29 @@ fn test_parallelize_no_descendants() { } test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=6"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ b911505e443e 6 - ◉ 2e00cb15c7b6 5 - ◉ 9df3c87db1a2 4 - ◉ 9f5b59fa4622 3 - ◉ d826910d21fb 2 - ◉ dc0e5d6135ce 1 - ◉ 000000000000 + @ b911505e443e 6 parents: 5 + ◉ 2e00cb15c7b6 5 parents: 4 + ◉ 9df3c87db1a2 4 parents: 3 + ◉ 9f5b59fa4622 3 parents: 2 + ◉ d826910d21fb 2 parents: 1 + ◉ dc0e5d6135ce 1 parents: + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok(&workspace_path, &["parallelize", "description(1)::"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 6c7b60a45eb6 6 - │ ◉ 296f48966777 5 + @ 6c7b60a45eb6 6 parents: + │ ◉ 296f48966777 5 parents: ├─╯ - │ ◉ 524062469789 4 + │ ◉ 524062469789 4 parents: ├─╯ - │ ◉ a9334ecaa379 3 + │ ◉ a9334ecaa379 3 parents: ├─╯ - │ ◉ 3a7b37ebe843 2 + │ ◉ 3a7b37ebe843 2 parents: ├─╯ - │ ◉ 761e67df44b7 1 + │ ◉ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -65,13 +65,13 @@ fn test_parallelize_with_descendants_simple() { } test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=6"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ b911505e443e 6 - ◉ 2e00cb15c7b6 5 - ◉ 9df3c87db1a2 4 - ◉ 9f5b59fa4622 3 - ◉ d826910d21fb 2 - ◉ dc0e5d6135ce 1 - ◉ 000000000000 + @ b911505e443e 6 parents: 5 + ◉ 2e00cb15c7b6 5 parents: 4 + ◉ 9df3c87db1a2 4 parents: 3 + ◉ 9f5b59fa4622 3 parents: 2 + ◉ d826910d21fb 2 parents: 1 + ◉ dc0e5d6135ce 1 parents: + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok( @@ -79,17 +79,17 @@ fn test_parallelize_with_descendants_simple() { &["parallelize", "description(1)::description(4)"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ f28f986c7134 6 - ◉ 21e9963ac5ff 5 + @ 259d624373d7 6 parents: 5 + ◉ 60d419591c77 5 parents: 1 2 3 4 ├─┬─┬─╮ - │ │ │ ◉ 524062469789 4 - │ │ ◉ │ a9334ecaa379 3 + │ │ │ ◉ 524062469789 4 parents: + │ │ ◉ │ a9334ecaa379 3 parents: │ │ ├─╯ - │ ◉ │ 3a7b37ebe843 2 + │ ◉ │ 3a7b37ebe843 2 parents: │ ├─╯ - ◉ │ 761e67df44b7 1 + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -108,32 +108,32 @@ fn test_parallelize_where_interior_has_non_target_children() { test_env.jj_cmd_ok(&workspace_path, &["new", "description(2)", "-m=2c"]); test_env.jj_cmd_ok(&workspace_path, &["new", "description(5)", "-m=6"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ d27ee705f7a9 6 - ◉ 2e00cb15c7b6 5 - ◉ 9df3c87db1a2 4 - ◉ 9f5b59fa4622 3 - │ ◉ 9c8865930f3c 2c - ├─╯ - ◉ d826910d21fb 2 - ◉ dc0e5d6135ce 1 - ◉ 000000000000 + @ d27ee705f7a9 6 parents: 5 + ◉ 2e00cb15c7b6 5 parents: 4 + ◉ 9df3c87db1a2 4 parents: 3 + ◉ 9f5b59fa4622 3 parents: 2 + │ ◉ 9c8865930f3c 2c parents: 2 + ├─╯ + ◉ d826910d21fb 2 parents: 1 + ◉ dc0e5d6135ce 1 parents: + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok(&workspace_path, &["parallelize", "dc0::9df"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 9f1bec0d6c46 6 - ◉ 7dd2f5648395 5 + @ a42de3959cae 6 parents: 5 + ◉ d907c901bad0 5 parents: 1 2 3 4 ├─┬─┬─╮ - │ │ │ ◉ b8f977c12383 4 - │ │ ◉ │ 7be8374575b9 3 + │ │ │ ◉ b8f977c12383 4 parents: + │ │ ◉ │ 7be8374575b9 3 parents: │ │ ├─╯ - │ │ │ ◉ 679fc870858c 2c + │ │ │ ◉ 2a4c3dab2a50 2c parents: 1 2 ╭─┬───╯ - │ ◉ │ 96ce11389312 2 + │ ◉ │ 96ce11389312 2 parents: │ ├─╯ - ◉ │ 2bfe3fe3e472 1 + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -149,28 +149,29 @@ fn test_parallelize_where_root_has_non_target_children() { test_env.jj_cmd_ok(&workspace_path, &["new", "description(1)", "-m=1c"]); test_env.jj_cmd_ok(&workspace_path, &["new", "description(3)", "-m=4"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 7636b3f489f4 4 - ◉ 9f5b59fa4622 3 - ◉ d826910d21fb 2 - │ ◉ 50e2ced81124 1c + @ 7636b3f489f4 4 parents: 3 + ◉ 9f5b59fa4622 3 parents: 2 + ◉ d826910d21fb 2 parents: 1 + │ ◉ 50e2ced81124 1c parents: 1 ├─╯ - ◉ dc0e5d6135ce 1 - ◉ 000000000000 + ◉ dc0e5d6135ce 1 parents: + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok( &workspace_path, &["parallelize", "description(1)::description(3)"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - ◉ ad35c9caf4fb 1c - │ @ 6ee674074e23 4 - ╭─┼─╮ - │ │ ◉ 5bd049136a7c 3 - │ ◉ │ 60f737a5a4a7 2 + @ d024344469c3 4 parents: 1 2 3 + ├─┬─╮ + │ │ ◉ 5bd049136a7c 3 parents: + │ ◉ │ 60f737a5a4a7 2 parents: │ ├─╯ - ◉ │ 79ebcd81a1ee 1 + │ │ ◉ 50e2ced81124 1c parents: 1 + ├───╯ + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -192,15 +193,15 @@ fn test_parallelize_with_merge_commit_child() { ); test_env.jj_cmd_ok(&workspace_path, &["new", "description(3)", "-m", "4"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 90a65779e2ec 4 - ◉ 9f5b59fa4622 3 - │ ◉ a01c1fad8506 2a-c + @ 90a65779e2ec 4 parents: 3 + ◉ 9f5b59fa4622 3 parents: 2 + │ ◉ a01c1fad8506 2a-c parents: 2 a ╭─┤ - │ ◉ 1eb902150bb9 a - ◉ │ d826910d21fb 2 - ◉ │ dc0e5d6135ce 1 + │ ◉ 1eb902150bb9 a parents: + ◉ │ d826910d21fb 2 parents: 1 + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); // After this finishes, child-2a will have three parents: "1", "2", and "a". @@ -209,23 +210,23 @@ fn test_parallelize_with_merge_commit_child() { &["parallelize", "description(1)::description(3)"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 5a0dd49510d1 4 + @ 6107429ab54b 4 parents: 1 2 3 ├─┬─╮ - │ │ ◉ a9334ecaa379 3 - │ │ │ ◉ 605371712469 2a-c + │ │ ◉ a9334ecaa379 3 parents: + │ │ │ ◉ a386386b94bc 2a-c parents: 1 2 a ╭─┬───┤ - │ │ │ ◉ 1eb902150bb9 a + │ │ │ ◉ 1eb902150bb9 a parents: │ │ ├─╯ - │ ◉ │ 3a7b37ebe843 2 + │ ◉ │ 3a7b37ebe843 2 parents: │ ├─╯ - ◉ │ 761e67df44b7 1 + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } #[test] -fn test_parallelize_failure_disconnected_target_commits() { +fn test_parallelize_disconnected_target_commits() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); let workspace_path = test_env.env_root().join("repo"); @@ -235,15 +236,25 @@ fn test_parallelize_failure_disconnected_target_commits() { } test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=3"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 9f5b59fa4622 3 - ◉ d826910d21fb 2 - ◉ dc0e5d6135ce 1 - ◉ 000000000000 + @ 9f5b59fa4622 3 parents: 2 + ◉ d826910d21fb 2 parents: 1 + ◉ dc0e5d6135ce 1 parents: + ◉ 000000000000 parents: "###); - insta::assert_snapshot!(test_env.jj_cmd_failure( - &workspace_path, &["parallelize", "description(1)", "description(3)"]),@r###" - Error: Cannot parallelize since the target revisions are not connected. + let (stdout, stderr) = test_env.jj_cmd_ok( + &workspace_path, + &["parallelize", "description(1)", "description(3)"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Nothing changed. + "###); + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" + @ 9f5b59fa4622 3 parents: 2 + ◉ d826910d21fb 2 parents: 1 + ◉ dc0e5d6135ce 1 parents: + ◉ 000000000000 parents: "###); } @@ -252,6 +263,7 @@ fn test_parallelize_head_is_a_merge() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); let workspace_path = test_env.env_root().join("repo"); + test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=0"]); test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=1"]); test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=2"]); test_env.jj_cmd_ok(&workspace_path, &["new", "root()"]); @@ -262,19 +274,30 @@ fn test_parallelize_head_is_a_merge() { &["new", "description(2)", "description(b)", "-m=merged-head"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 1a8db14a8cf0 merged-head + @ f2087b66e475 merged-head parents: 2 b ├─╮ - │ ◉ 401e43e9461f b - │ ◉ 66ea2ab19a70 a - ◉ │ d826910d21fb 2 - ◉ │ dc0e5d6135ce 1 + │ ◉ 5164ab888473 b parents: a + │ ◉ f16fe8ac5ce9 a parents: + ◉ │ fe79412860e8 2 parents: 1 + ◉ │ a915696cf0ad 1 parents: 0 + ◉ │ a56846756248 0 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); - insta::assert_snapshot!(test_env.jj_cmd_failure(&workspace_path,&["parallelize", "description(1)::"]), - @r###" - Error: Only the roots of the target revset are allowed to have parents which are not being parallelized. + test_env.jj_cmd_ok(&workspace_path, &["parallelize", "description(1)::"]); + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" + @ babb4191912d merged-head parents: 0 b + ├─╮ + │ ◉ 5164ab888473 b parents: a + │ ◉ f16fe8ac5ce9 a parents: + │ │ ◉ 36b2f866a798 2 parents: 0 + ├───╯ + │ │ ◉ a915696cf0ad 1 parents: 0 + ├───╯ + ◉ │ a56846756248 0 parents: + ├─╯ + ◉ 000000000000 parents: "###); } @@ -283,6 +306,7 @@ fn test_parallelize_interior_target_is_a_merge() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); let workspace_path = test_env.env_root().join("repo"); + test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=0"]); test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=1"]); test_env.jj_cmd_ok(&workspace_path, &["new", "root()", "-m=a"]); test_env.jj_cmd_ok( @@ -291,18 +315,28 @@ fn test_parallelize_interior_target_is_a_merge() { ); test_env.jj_cmd_ok(&workspace_path, &["new", "-m=3"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 299099c22761 3 - ◉ 0c4da981fc0a 2 + @ a6321093e3d3 3 parents: 2 + ◉ 705c32f67ce1 2 parents: 1 a ├─╮ - │ ◉ 6d37472c632c a - ◉ │ dc0e5d6135ce 1 + │ ◉ 427890ea3f2b a parents: + ◉ │ a915696cf0ad 1 parents: 0 + ◉ │ a56846756248 0 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); - insta::assert_snapshot!(test_env.jj_cmd_failure(&workspace_path,&["parallelize", "description(1)::"]), - @r###" - Error: Only the roots of the target revset are allowed to have parents which are not being parallelized. + test_env.jj_cmd_ok(&workspace_path, &["parallelize", "description(1)::"]); + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" + @ cd0ac6ad1415 3 parents: 0 a + ├─╮ + │ │ ◉ 1c240e875670 2 parents: 0 a + ╭─┬─╯ + │ ◉ 427890ea3f2b a parents: + │ │ ◉ a915696cf0ad 1 parents: 0 + ├───╯ + ◉ │ a56846756248 0 parents: + ├─╯ + ◉ 000000000000 parents: "###); } @@ -320,14 +354,14 @@ fn test_parallelize_root_is_a_merge() { test_env.jj_cmd_ok(&workspace_path, &["new", "-m=2"]); test_env.jj_cmd_ok(&workspace_path, &["new", "-m=3"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 9f66b50aa1f2 3 - ◉ dd995ce87f21 2 - ◉ 4b4941342e06 1 + @ 9f66b50aa1f2 3 parents: 2 + ◉ dd995ce87f21 2 parents: 1 + ◉ 4b4941342e06 1 parents: y x ├─╮ - │ ◉ 4035b23c8f72 x - ◉ │ f3ec359cf9ff y + │ ◉ 4035b23c8f72 x parents: + ◉ │ f3ec359cf9ff y parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok( @@ -335,16 +369,16 @@ fn test_parallelize_root_is_a_merge() { &["parallelize", "description(1)::description(2)"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 4e81469adb0d 3 + @ d6df04b236b0 3 parents: 1 2 ├─╮ - │ ◉ 38945baf55f4 2 + │ ◉ 38945baf55f4 2 parents: y x │ ├─╮ - ◉ │ │ 9b1a1927720c 1 + ◉ │ │ 4b4941342e06 1 parents: y x ╰─┬─╮ - │ ◉ 4035b23c8f72 x - ◉ │ f3ec359cf9ff y + │ ◉ 4035b23c8f72 x parents: + ◉ │ f3ec359cf9ff y parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -357,21 +391,21 @@ fn test_parallelize_multiple_heads() { test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=1"]); test_env.jj_cmd_ok(&workspace_path, &["new", "description(0)", "-m=2"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 8314addde180 2 - │ ◉ a915696cf0ad 1 + @ 8314addde180 2 parents: 0 + │ ◉ a915696cf0ad 1 parents: 0 ├─╯ - ◉ a56846756248 0 - ◉ 000000000000 + ◉ a56846756248 0 parents: + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok(&workspace_path, &["parallelize", "description(0)::"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ e84481c26195 2 - │ ◉ 2047527ade93 1 + @ e84481c26195 2 parents: + │ ◉ 2047527ade93 1 parents: ├─╯ - │ ◉ 9d0c0750973c 0 + │ ◉ a56846756248 0 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -387,11 +421,11 @@ fn test_parallelize_multiple_heads_with_and_without_children() { test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=1"]); test_env.jj_cmd_ok(&workspace_path, &["new", "description(0)", "-m=2"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 8314addde180 2 - │ ◉ a915696cf0ad 1 + @ 8314addde180 2 parents: 0 + │ ◉ a915696cf0ad 1 parents: 0 ├─╯ - ◉ a56846756248 0 - ◉ 000000000000 + ◉ a56846756248 0 parents: + ◉ 000000000000 parents: "###); test_env.jj_cmd_ok( @@ -399,11 +433,11 @@ fn test_parallelize_multiple_heads_with_and_without_children() { &["parallelize", "description(0)", "description(1)"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 49fe9e130d15 2 - ◉ 9d0c0750973c 0 - │ ◉ 2047527ade93 1 + ◉ 2047527ade93 1 parents: + │ @ 8314addde180 2 parents: 0 + │ ◉ a56846756248 0 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } @@ -420,31 +454,31 @@ fn test_parallelize_multiple_roots() { ); test_env.jj_cmd_ok(&workspace_path, &["new", "-m=3"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 299099c22761 3 - ◉ 0c4da981fc0a 2 + @ 299099c22761 3 parents: 2 + ◉ 0c4da981fc0a 2 parents: 1 a ├─╮ - │ ◉ 6d37472c632c a - ◉ │ dc0e5d6135ce 1 + │ ◉ 6d37472c632c a parents: + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); // Succeeds because the roots have the same parents. test_env.jj_cmd_ok(&workspace_path, &["parallelize", "root().."]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 3c90598481cd 3 - │ ◉ b96aa55582e5 2 + @ 3c90598481cd 3 parents: + │ ◉ b96aa55582e5 2 parents: ├─╯ - │ ◉ 3178394e33e7 a + │ ◉ 6d37472c632c a parents: ├─╯ - │ ◉ 1d9a0895e7d6 1 + │ ◉ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); } #[test] -fn test_parallelize_failure_multiple_heads_with_different_children() { +fn test_parallelize_multiple_heads_with_different_children() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); let workspace_path = test_env.env_root().join("repo"); @@ -456,32 +490,44 @@ fn test_parallelize_failure_multiple_heads_with_different_children() { test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=b"]); test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=c"]); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 9b5fa4b364d4 - ◉ 7b095ae9b21f c - ◉ 5164ab888473 b - ◉ f16fe8ac5ce9 a - │ ◉ 9f5b59fa4622 3 - │ ◉ d826910d21fb 2 - │ ◉ dc0e5d6135ce 1 - ├─╯ - ◉ 000000000000 + @ 9b5fa4b364d4 parents: c + ◉ 7b095ae9b21f c parents: b + ◉ 5164ab888473 b parents: a + ◉ f16fe8ac5ce9 a parents: + │ ◉ 9f5b59fa4622 3 parents: 2 + │ ◉ d826910d21fb 2 parents: 1 + │ ◉ dc0e5d6135ce 1 parents: + ├─╯ + ◉ 000000000000 parents: "###); - insta::assert_snapshot!( - test_env.jj_cmd_failure( + test_env.jj_cmd_ok( &workspace_path, &[ "parallelize", "description(1)::description(2)", "description(a)::description(b)", ], - ),@r###" - Error: All heads of the target revisions must have the same children. + ); + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" + @ 582c6bd1e1fd parents: c + ◉ dd2db8b60a69 c parents: a b + ├─╮ + │ ◉ 190b857f6cdd b parents: + ◉ │ f16fe8ac5ce9 a parents: + ├─╯ + │ ◉ bbc313370f45 3 parents: 1 2 + │ ├─╮ + │ │ ◉ 96ce11389312 2 parents: + ├───╯ + │ ◉ dc0e5d6135ce 1 parents: + ├─╯ + ◉ 000000000000 parents: "###); } #[test] -fn test_parallelize_failure_multiple_roots_with_different_parents() { +fn test_parallelize_multiple_roots_with_different_parents() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); let workspace_path = test_env.env_root().join("repo"); @@ -495,22 +541,31 @@ fn test_parallelize_failure_multiple_roots_with_different_parents() { &["new", "description(2)", "description(b)", "-m=merged-head"], ); insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" - @ 1a8db14a8cf0 merged-head + @ 1a8db14a8cf0 merged-head parents: 2 b ├─╮ - │ ◉ 401e43e9461f b - │ ◉ 66ea2ab19a70 a - ◉ │ d826910d21fb 2 - ◉ │ dc0e5d6135ce 1 + │ ◉ 401e43e9461f b parents: a + │ ◉ 66ea2ab19a70 a parents: + ◉ │ d826910d21fb 2 parents: 1 + ◉ │ dc0e5d6135ce 1 parents: ├─╯ - ◉ 000000000000 + ◉ 000000000000 parents: "###); - insta::assert_snapshot!( - test_env.jj_cmd_failure( + test_env.jj_cmd_ok( &workspace_path, &["parallelize", "description(2)::", "description(b)::"], - ),@r###" - Error: All roots of the target revisions must have the same parents. + ); + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" + @ 4224f9c9e598 merged-head parents: 1 a + ├─╮ + │ │ ◉ 401e43e9461f b parents: a + │ ├─╯ + │ ◉ 66ea2ab19a70 a parents: + │ │ ◉ d826910d21fb 2 parents: 1 + ├───╯ + ◉ │ dc0e5d6135ce 1 parents: + ├─╯ + ◉ 000000000000 parents: "###); } @@ -527,7 +582,7 @@ fn test_parallelize_complex_nonlinear_target() { test_env.jj_cmd_ok(&workspace_path, &["new", "-m=1c", "description(1)"]); test_env.jj_cmd_ok(&workspace_path, &["new", "-m=2c", "description(2)"]); test_env.jj_cmd_ok(&workspace_path, &["new", "-m=3c", "description(3)"]); - insta::assert_snapshot!(get_log_output_with_parents(&test_env, &workspace_path), @r###" + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" @ b043eb81416c 3c parents: 3 │ ◉ 48277ee9afe0 4 parents: 3 2 1 ╭─┼─╮ @@ -549,23 +604,23 @@ fn test_parallelize_complex_nonlinear_target() { &["parallelize", "description(0)::description(4)"], ); insta::assert_snapshot!(stderr, @r###" - Working copy now at: yostqsxw d193f3b7 (empty) 3c - Parent commit : rlvkpnrz cbb4e169 (empty) 0 + Working copy now at: yostqsxw 59a216e5 (empty) 3c + Parent commit : rlvkpnrz 745bea80 (empty) 0 Parent commit : mzvwutvl cb944786 (empty) 3 "###); - insta::assert_snapshot!(get_log_output_with_parents(&test_env, &workspace_path), @r###" - @ d193f3b72495 3c parents: 0 3 + insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###" + @ 59a216e537c4 3c parents: 0 3 ├─╮ │ ◉ cb9447869bf0 3 parents: - │ │ ◉ 80fbafb56917 2c parents: 0 2 + │ │ ◉ 248ce1ffd76b 2c parents: 0 2 ╭───┤ │ │ ◉ 8f4b8ef68676 2 parents: │ ├─╯ - │ │ ◉ 1985e0427139 1c parents: 0 1 + │ │ ◉ 55c626d090e2 1c parents: 0 1 ╭───┤ │ │ ◉ 82918d78c984 1 parents: │ ├─╯ - ◉ │ cbb4e1692ef4 0 parents: + ◉ │ 745bea8029c1 0 parents: ├─╯ │ ◉ 14ca4df576b3 4 parents: ├─╯ @@ -573,7 +628,7 @@ fn test_parallelize_complex_nonlinear_target() { "###) } -fn get_log_output_with_parents(test_env: &TestEnvironment, cwd: &Path) -> String { +fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { let template = r#" separate(" ", commit_id.short(), @@ -583,8 +638,3 @@ fn get_log_output_with_parents(test_env: &TestEnvironment, cwd: &Path) -> String )"#; test_env.jj_cmd_success(cwd, &["log", "-T", template]) } - -fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { - let template = r#"separate(" ", commit_id.short(), local_branches, description)"#; - test_env.jj_cmd_success(cwd, &["log", "-T", template]) -} diff --git a/lib/src/repo.rs b/lib/src/repo.rs index d136babbfe..6e01b528b8 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -51,7 +51,7 @@ use crate::refs::{ diff_named_ref_targets, diff_named_remote_refs, merge_ref_targets, merge_remote_refs, }; use crate::revset::{RevsetEvaluationError, RevsetExpression, RevsetIteratorExt}; -use crate::rewrite::{DescendantRebaser, RebaseOptions}; +use crate::rewrite::{CommitRewriter, DescendantRebaser, RebaseOptions}; use crate::settings::{RepoSettings, UserSettings}; use crate::signing::{SignInitError, Signer}; use crate::simple_op_heads_store::SimpleOpHeadsStore; @@ -926,7 +926,7 @@ impl MutableRepo { /// rewritten and abandoned. /// /// Panics if `parent_mapping` contains cycles - pub fn new_parents(&self, old_ids: &[CommitId]) -> Vec { + pub fn new_parents(&self, old_ids: Vec) -> Vec { fn single_substitution_round( parent_mapping: &HashMap)>, ids: Vec, @@ -958,7 +958,7 @@ impl MutableRepo { (new_ids, made_replacements) } - let mut new_ids: Vec = old_ids.to_vec(); + let mut new_ids = old_ids; // The longest possible non-cycle substitution sequence goes through each key of // parent_mapping once. let mut allowed_iterations = 0..self.parent_mapping.len(); @@ -995,7 +995,7 @@ impl MutableRepo { // mappings, not transitive ones. // TODO: keep parent_mapping updated with transitive mappings so we don't need // to call `new_parents()` here. - let new_parent_ids = self.new_parents(&new_parent_ids); + let new_parent_ids = self.new_parents(new_parent_ids); self.update_references(settings, old_parent_id, new_parent_ids)?; } Ok(()) @@ -1154,6 +1154,42 @@ impl MutableRepo { ) } + /// Rewrite descendants of the given roots. + /// + /// The callback will be called for each commit with the new parents + /// prepopulated. The callback may change the parents and write the new + /// commit, or it may abandon the commit, or it may leave the old commit + /// unchanged. + /// + /// The set of commits to visit is determined at the start. If the callback + /// adds new descendants, then the callback will not be called for those. + /// Similarly, if the callback rewrites unrelated commits, then the callback + /// will not be called for descendants of those commits. + pub fn transform_descendants( + &mut self, + settings: &UserSettings, + roots: Vec, + mut callback: impl FnMut(CommitRewriter) -> BackendResult<()>, + ) -> BackendResult<()> { + let mut to_visit = self.find_descendants_to_rebase(roots)?; + while let Some(old_commit) = to_visit.pop() { + let new_parent_ids = self.new_parents(old_commit.parent_ids().to_vec()); + let rewriter = CommitRewriter::new(self, old_commit, new_parent_ids); + callback(rewriter)?; + } + self.update_rewritten_references(settings)?; + // Since we didn't necessarily visit all descendants of rewritten commits (e.g. + // if they were rewritten in the callback), there can still be commits left to + // rebase, so we don't clear `parent_mapping` here. + // TODO: Should we make this stricter? We could check that there were no + // rewrites before this function was called, and we can check that only + // commits in the `to_visit` set were added by the callback. Then we + // could clear `parent_mapping` here and not have to scan it again at + // the end of the transaction when we call `rebase_descendants()`. + + Ok(()) + } + /// After the rebaser returned by this function is dropped, /// self.parent_mapping needs to be cleared. fn rebase_descendants_return_rebaser<'settings, 'repo>( @@ -1202,6 +1238,7 @@ impl MutableRepo { /// **its parent**. One can tell this case apart since the change ids of the /// key and the value will not match. The parent will inherit the /// descendants and the branches of the abandoned commit. + // TODO: Rewrite this using `transform_descendants()` pub fn rebase_descendants_with_options_return_map( &mut self, settings: &UserSettings, @@ -1218,7 +1255,18 @@ impl MutableRepo { } pub fn rebase_descendants(&mut self, settings: &UserSettings) -> BackendResult { - self.rebase_descendants_with_options(settings, Default::default()) + let roots = self.parent_mapping.keys().cloned().collect_vec(); + let mut num_rebased = 0; + self.transform_descendants(settings, roots, |rewriter| { + if rewriter.parents_changed() { + let builder = rewriter.rebase(settings)?; + builder.write()?; + num_rebased += 1; + } + Ok(()) + })?; + self.parent_mapping.clear(); + Ok(num_rebased) } pub fn rebase_descendants_return_map( diff --git a/lib/src/rewrite.rs b/lib/src/rewrite.rs index c9249459bd..c66c8dc015 100644 --- a/lib/src/rewrite.rs +++ b/lib/src/rewrite.rs @@ -145,6 +145,23 @@ impl<'repo> CommitRewriter<'repo> { self.new_parents = new_parents; } + /// Set the old commit's intended new parents to be the rewritten versions + /// of the given parents. + pub fn set_new_rewritten_parents(&mut self, unrewritten_parents: Vec) { + self.new_parents = self.mut_repo.new_parents(unrewritten_parents); + } + + /// Update the intended new parents by replacing any occurrence of + /// `old_parent` by `new_parents`. + pub fn replace_parent(&mut self, old_parent: &CommitId, new_parents: &[CommitId]) { + if let Some(i) = self.new_parents.iter().position(|p| p == old_parent) { + self.new_parents + .splice(i..i + 1, new_parents.iter().cloned()); + let mut unique = HashSet::new(); + self.new_parents.retain(|p| unique.insert(p.clone())); + } + } + /// Checks if the intended new parents are different from the old commit's /// parents. pub fn parents_changed(&self) -> bool { @@ -384,7 +401,7 @@ impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> { fn rebase_one(&mut self, old_commit: Commit) -> BackendResult<()> { let old_commit_id = old_commit.id().clone(); let old_parent_ids = old_commit.parent_ids(); - let new_parent_ids = self.mut_repo.new_parents(old_parent_ids); + let new_parent_ids = self.mut_repo.new_parents(old_parent_ids.to_vec()); let rewriter = CommitRewriter::new(self.mut_repo, old_commit, new_parent_ids); if !rewriter.parents_changed() { // The commit is already in place. diff --git a/lib/tests/runner.rs b/lib/tests/runner.rs index 44f939ef3d..03f134bbde 100644 --- a/lib/tests/runner.rs +++ b/lib/tests/runner.rs @@ -29,6 +29,7 @@ mod test_operations; mod test_refs; mod test_revset; mod test_rewrite; +mod test_rewrite_transform; mod test_signing; mod test_ssh_signing; mod test_view; diff --git a/lib/tests/test_rewrite_transform.rs b/lib/tests/test_rewrite_transform.rs new file mode 100644 index 0000000000..2a7c804b8d --- /dev/null +++ b/lib/tests/test_rewrite_transform.rs @@ -0,0 +1,127 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use jj_lib::repo::Repo; +use maplit::hashset; +use testutils::{CommitGraphBuilder, TestRepo}; + +// Simulate some `jj sync` command that rebases B:: onto G while abandoning C +// (because it's presumably already in G). +// +// G +// | E +// | D F +// | |/ +// | C +// | B +// |/ +// A +#[test] +fn test_transform_descendants_sync() { + let settings = testutils::user_settings(); + let test_repo = TestRepo::init(); + let repo = &test_repo.repo; + + let mut tx = repo.start_transaction(&settings); + let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo()); + let commit_a = graph_builder.initial_commit(); + let commit_b = graph_builder.commit_with_parents(&[&commit_a]); + let commit_c = graph_builder.commit_with_parents(&[&commit_b]); + let commit_d = graph_builder.commit_with_parents(&[&commit_c]); + let commit_e = graph_builder.commit_with_parents(&[&commit_d]); + let commit_f = graph_builder.commit_with_parents(&[&commit_c]); + let commit_g = graph_builder.commit_with_parents(&[&commit_a]); + + let mut rebased = HashMap::new(); + tx.mut_repo() + .transform_descendants(&settings, vec![commit_b.id().clone()], |mut rewriter| { + rewriter.replace_parent(commit_a.id(), &[commit_g.id().clone()]); + if *rewriter.old_commit() == commit_c { + let old_id = rewriter.old_commit().id().clone(); + let new_parent_ids = rewriter.new_parents().to_vec(); + rewriter + .mut_repo() + .record_abandoned_commit_with_parents(old_id, new_parent_ids); + } else { + let old_commit_id = rewriter.old_commit().id().clone(); + let new_commit = rewriter.rebase(&settings)?.write()?; + rebased.insert(old_commit_id, new_commit); + } + Ok(()) + }) + .unwrap(); + assert_eq!(rebased.len(), 4); + let new_commit_b = rebased.get(commit_b.id()).unwrap(); + let new_commit_d = rebased.get(commit_d.id()).unwrap(); + let new_commit_e = rebased.get(commit_e.id()).unwrap(); + let new_commit_f = rebased.get(commit_f.id()).unwrap(); + + assert_eq!( + *tx.mut_repo().view().heads(), + hashset! { + new_commit_e.id().clone(), + new_commit_f.id().clone(), + } + ); + + assert_eq!(new_commit_b.parent_ids(), vec![commit_g.id().clone()]); + assert_eq!(new_commit_d.parent_ids(), vec![new_commit_b.id().clone()]); + assert_eq!(new_commit_e.parent_ids(), vec![new_commit_d.id().clone()]); + assert_eq!(new_commit_f.parent_ids(), vec![new_commit_b.id().clone()]); +} + +// Transform just commit C replacing parent A by parent B. The parents should be +// deduplicated. +// +// C +// /| +// B | +// |/ +// A +#[test] +fn test_transform_descendants_sync_linearize_merge() { + let settings = testutils::user_settings(); + let test_repo = TestRepo::init(); + let repo = &test_repo.repo; + + let mut tx = repo.start_transaction(&settings); + let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo()); + let commit_a = graph_builder.initial_commit(); + let commit_b = graph_builder.commit_with_parents(&[&commit_a]); + let commit_c = graph_builder.commit_with_parents(&[&commit_a, &commit_b]); + + let mut rebased = HashMap::new(); + tx.mut_repo() + .transform_descendants(&settings, vec![commit_c.id().clone()], |mut rewriter| { + rewriter.replace_parent(commit_a.id(), &[commit_b.id().clone()]); + let old_commit_id = rewriter.old_commit().id().clone(); + let new_commit = rewriter.rebase(&settings)?.write()?; + rebased.insert(old_commit_id, new_commit); + Ok(()) + }) + .unwrap(); + assert_eq!(rebased.len(), 1); + let new_commit_c = rebased.get(commit_c.id()).unwrap(); + + assert_eq!( + *tx.mut_repo().view().heads(), + hashset! { + new_commit_c.id().clone(), + } + ); + + assert_eq!(new_commit_c.parent_ids(), vec![commit_b.id().clone()]); +}