From cc677ec35dff04a1157a68cd1cf85e298109b447 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 21 Apr 2024 19:59:12 +0800 Subject: [PATCH] rebase: rewrite `rebase_revision` to use `transform_descendants` --- cli/src/commands/rebase.rs | 119 +++++++++---------------------- cli/tests/common/mod.rs | 1 + cli/tests/test_rebase_command.rs | 100 ++++++-------------------- 3 files changed, 57 insertions(+), 163 deletions(-) diff --git a/cli/src/commands/rebase.rs b/cli/src/commands/rebase.rs index 8b6ffc68cd..b5aa4211cc 100644 --- a/cli/src/commands/rebase.rs +++ b/cli/src/commands/rebase.rs @@ -20,7 +20,6 @@ use std::sync::Arc; use clap::ArgGroup; use indexmap::IndexSet; use itertools::Itertools; -use jj_lib::backend::CommitId; use jj_lib::commit::{Commit, CommitIteratorExt}; use jj_lib::object_id::ObjectId; use jj_lib::repo::{ReadonlyRepo, Repo}; @@ -356,96 +355,48 @@ fn rebase_revision( new_parents: &[Commit], rev_arg: &RevisionArg, ) -> Result<(), CommandError> { - let old_commit = workspace_command.resolve_single_rev(rev_arg)?; - workspace_command.check_rewritable([old_commit.id()])?; - if new_parents.contains(&old_commit) { + let to_rebase_commit = workspace_command.resolve_single_rev(rev_arg)?; + workspace_command.check_rewritable([to_rebase_commit.id()])?; + if new_parents.contains(&to_rebase_commit) { return Err(user_error(format!( "Cannot rebase {} onto itself", - short_commit_hash(old_commit.id()), + short_commit_hash(to_rebase_commit.id()), ))); } - let children_expression = RevsetExpression::commit(old_commit.id().clone()).children(); - let child_commits: Vec<_> = children_expression - .evaluate_programmatic(workspace_command.repo().as_ref()) - .unwrap() - .iter() - .commits(workspace_command.repo().store()) - .try_collect()?; - // Currently, immutable commits are defined so that a child of a rewriteable - // commit is always rewriteable. - debug_assert!(workspace_command - .check_rewritable(child_commits.iter().ids()) - .is_ok()); - - // First, rebase the children of `old_commit`. let mut tx = workspace_command.start_transaction(); let mut rebased_commit_ids = HashMap::new(); - for child_commit in child_commits { - let new_child_parent_ids: Vec = child_commit - .parents() - .iter() - .flat_map(|c| { - if c == &old_commit { - old_commit.parent_ids().to_vec() - } else { - [c.id().clone()].to_vec() - } - }) - .collect(); - - // Some of the new parents may be ancestors of others as in - // `test_rebase_single_revision`. - let new_child_parents_expression = RevsetExpression::commits(new_child_parent_ids.clone()) - .minus( - &RevsetExpression::commits(new_child_parent_ids.clone()) - .parents() - .ancestors(), - ); - let new_child_parents = new_child_parents_expression - .evaluate_programmatic(tx.base_repo().as_ref()) - .unwrap() - .iter() - .collect_vec(); - rebased_commit_ids.insert( - child_commit.id().clone(), - rebase_commit(settings, tx.mut_repo(), child_commit, new_child_parents)? - .id() - .clone(), - ); - } - // Now, rebase the descendants of the children. + // First, rebase the descendants of `to_rebase_commit`. // TODO(ilyagr): Consider making it possible for these descendants to become // emptied, like --skip_empty. This would require writing careful tests. - rebased_commit_ids.extend(tx.mut_repo().rebase_descendants_return_map(settings)?); + tx.mut_repo().transform_descendants( + settings, + vec![to_rebase_commit.id().clone()], + |mut rewriter| { + let old_commit = rewriter.old_commit(); + let old_commit_id = old_commit.id().clone(); + + // Replace references to `to_rebase_commit` with its parents. + if old_commit.parent_ids().contains(to_rebase_commit.id()) { + rewriter.replace_parent(to_rebase_commit.id(), to_rebase_commit.parent_ids()); + rewriter.simplify_ancestor_merge(); + } + if rewriter.parents_changed() { + let builder = rewriter.rebase(settings)?; + let commit = builder.write()?; + rebased_commit_ids.insert(old_commit_id, commit.id().clone()); + } + Ok(()) + }, + )?; + let num_rebased_descendants = rebased_commit_ids.len(); // We now update `new_parents` to account for the rebase of all of - // `old_commit`'s descendants. Even if some of the original `new_parents` - // were descendants of `old_commit`, this will no longer be the case after + // `to_rebase_commit`'s descendants. Even if some of the original `new_parents` + // were descendants of `to_rebase_commit`, this will no longer be the case after // the update. - // - // To make the update simpler, we assume that each commit was rewritten only - // once; we don't have a situation where both `(A,B)` and `(B,C)` are in - // `rebased_commit_ids`. - // - // TODO(BUG #2650): There is something wrong with this assumption, the next TODO - // seems to be a little optimistic. See the panicked test in - // `test_rebase_with_child_and_descendant_bug_2600`. - // - // TODO(ilyagr): This assumption relies on the fact that, after - // `rebase_descendants`, a descendant of `old_commit` cannot also be a - // direct child of `old_commit`. This fact will likely change, see - // https://github.com/martinvonz/jj/issues/2600. So, the code needs to be - // updated before that happens. This would also affect - // `test_rebase_with_child_and_descendant_bug_2600`. - // - // The issue is that if a child and a descendant of `old_commit` were the - // same commit (call it `Q`), it would be rebased first by `rebase_commit` - // above, and then the result would be rebased again by - // `rebase_descendants_return_map`. Then, if we were trying to rebase - // `old_commit` onto `Q`, new_parents would only account for one of these. let new_parents = new_parents .iter() .map(|new_parent| { @@ -456,20 +407,20 @@ fn rebase_revision( }) .collect(); - let tx_description = format!("rebase commit {}", old_commit.id().hex()); - // Finally, it's safe to rebase `old_commit`. We can skip rebasing if it is - // already a child of `new_parents`. Otherwise, at this point, it should no - // longer have any children; they have all been rebased and the originals + let tx_description = format!("rebase commit {}", to_rebase_commit.id().hex()); + // Finally, it's safe to rebase `to_rebase_commit`. We can skip rebasing if it + // is already a child of `new_parents`. Otherwise, at this point, it should + // no longer have any children; they have all been rebased and the originals // have been abandoned. - let skipped_commit_rebase = if old_commit.parent_ids() == new_parents { + let skipped_commit_rebase = if to_rebase_commit.parent_ids() == new_parents { if let Some(mut formatter) = ui.status_formatter() { write!(formatter, "Skipping rebase of commit ")?; - tx.write_commit_summary(formatter.as_mut(), &old_commit)?; + tx.write_commit_summary(formatter.as_mut(), &to_rebase_commit)?; writeln!(formatter)?; } true } else { - rebase_commit(settings, tx.mut_repo(), old_commit, new_parents)?; + rebase_commit(settings, tx.mut_repo(), to_rebase_commit, new_parents)?; debug_assert_eq!(tx.mut_repo().rebase_descendants(settings)?, 0); false }; diff --git a/cli/tests/common/mod.rs b/cli/tests/common/mod.rs index 880046ee61..72f4eb5a51 100644 --- a/cli/tests/common/mod.rs +++ b/cli/tests/common/mod.rs @@ -227,6 +227,7 @@ impl TestEnvironment { /// Run a `jj` command, check that it failed with code 101, and return its /// stderr #[must_use] + #[allow(dead_code)] pub fn jj_cmd_panic(&self, current_dir: &Path, args: &[&str]) -> String { let assert = self.jj_cmd(current_dir, args).assert().code(101).stdout(""); self.normalize_output(&get_stderr_string(&assert)) diff --git a/cli/tests/test_rebase_command.rs b/cli/tests/test_rebase_command.rs index 0dd41d2919..50e09cdae1 100644 --- a/cli/tests/test_rebase_command.rs +++ b/cli/tests/test_rebase_command.rs @@ -310,18 +310,18 @@ fn test_rebase_single_revision() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Also rebased 1 descendant commits onto parent of rebased commit - Working copy now at: vruxwmqv bf87078f d | d - Parent commit : zsuskuln d370aee1 b | b + Working copy now at: vruxwmqv ad122686 d | d Parent commit : rlvkpnrz 2443ea76 a | a + Parent commit : zsuskuln d370aee1 b | b Added 0 files, modified 0 files, removed 1 files "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ c │ @ d │ ├─╮ - │ │ ◉ a + │ │ ◉ b ├───╯ - │ ◉ b + │ ◉ a ├─╯ ◉ "###); @@ -354,17 +354,17 @@ fn test_rebase_single_revision_merge_parent() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Also rebased 1 descendant commits onto parent of rebased commit - Working copy now at: vruxwmqv c62d0789 d | d - Parent commit : zsuskuln d370aee1 b | b + Working copy now at: vruxwmqv a37531e8 d | d Parent commit : rlvkpnrz 2443ea76 a | a + Parent commit : zsuskuln d370aee1 b | b Added 0 files, modified 0 files, removed 1 files "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ c │ @ d ╭─┤ - ◉ │ a │ ◉ b + ◉ │ a ├─╯ ◉ "###); @@ -849,7 +849,7 @@ fn test_rebase_with_child_and_descendant_bug_2600() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Skipping rebase of commit rlvkpnrz 0c61db1b base | base - Rebased 4 descendant commits onto parent of commit + Rebased 3 descendant commits onto parent of commit Working copy now at: vruxwmqv 57aaa944 c | c Parent commit : royxmykx c8495a71 b | b Added 0 files, modified 0 files, removed 1 files @@ -872,74 +872,17 @@ fn test_rebase_with_child_and_descendant_bug_2600() { ◉ "###); - // TODO(#2650) !!!!! The panic here is a BUG !!! // This tests the algorithm for rebasing onto descendants. The result should be // simplified if and only if it's simplified in the above case. test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]); - let stderr = test_env.jj_cmd_panic(&repo_path, &["rebase", "-r", "base", "-d", "b"]); - assert!(stderr.contains("graph has cycle")); - // // At time of writing: - // insta::assert_snapshot!(stderr, @r###" - // thread 'main' panicked at lib/src/dag_walk.rs:113:13: - // graph has cycle - // stack backtrace: - // 0: rust_begin_unwind - // at - // /rustc/6cf088810f66fff15d05bf7135c5f5888b7c93b4/library/std/src/panicking.rs: - // 645:5 1: core::panicking::panic_fmt - // at - // /rustc/6cf088810f66fff15d05bf7135c5f5888b7c93b4/library/core/src/panicking. - // rs:72:14 2: jj_lib::dag_walk::topo_order_forward_ok - // at - // /usr/local/google/home/ilyagr/dev/jj/lib/src/dag_walk.rs:113:13 - // 3: jj_lib::dag_walk::topo_order_reverse_ok - // at - // /usr/local/google/home/ilyagr/dev/jj/lib/src/dag_walk.rs:160:22 - // 4: jj_lib::dag_walk::topo_order_reverse - // at - // /usr/local/google/home/ilyagr/dev/jj/lib/src/dag_walk.rs:142:5 - // 5: jj_lib::rewrite::DescendantRebaser::new - // at - // /usr/local/google/home/ilyagr/dev/jj/lib/src/rewrite.rs:306:24 - // 6: jj_lib::repo::MutableRepo::create_descendant_rebaser - // at /usr/local/google/home/ilyagr/dev/jj/lib/src/repo.rs:844:9 - // 7: jj_lib::repo::MutableRepo::rebase_descendants_return_rebaser - // at /usr/local/google/home/ilyagr/dev/jj/lib/src/repo.rs:861:27 - // 8: jj_lib::repo::MutableRepo::rebase_descendants_with_options - // at /usr/local/google/home/ilyagr/dev/jj/lib/src/repo.rs:872:12 - // 9: jj_lib::repo::MutableRepo::rebase_descendants - // at /usr/local/google/home/ilyagr/dev/jj/lib/src/repo.rs:878:9 - // 10: jj_cli::commands::rebase::rebase_revision - // at - // /usr/local/google/home/ilyagr/dev/jj/cli/src/commands/rebase.rs:420:22 - // 11: jj_cli::commands::rebase::cmd_rebase - // at - // /usr/local/google/home/ilyagr/dev/jj/cli/src/commands/rebase.rs:197:9 - // 12: jj_cli::commands::run_command - // at - // /usr/local/google/home/ilyagr/dev/jj/cli/src/commands/mod.rs:187:39 13: - // core::ops::function::FnOnce::call_once at - // /rustc/6cf088810f66fff15d05bf7135c5f5888b7c93b4/library/core/src/ops/ - // function.rs:250:5 14: core::ops::function::FnOnce::call_once{{vtable. - // shim}} at - // /rustc/6cf088810f66fff15d05bf7135c5f5888b7c93b4/library/core/src/ops/ - // function.rs:250:5 15: as - // core::ops::function::FnOnce>::call_once at - // /rustc/6cf088810f66fff15d05bf7135c5f5888b7c93b4/library/alloc/src/boxed.rs: - // 2007:9 16: jj_cli::cli_util::CliRunner::run_internal - // at - // /usr/local/google/home/ilyagr/dev/jj/cli/src/cli_util.rs:2867:9 - // 17: jj_cli::cli_util::CliRunner::run - // at - // /usr/local/google/home/ilyagr/dev/jj/cli/src/cli_util.rs:2884:22 - // 18: jj::main - // at /usr/local/google/home/ilyagr/dev/jj/cli/src/main.rs:18:5 - // 19: core::ops::function::FnOnce::call_once - // at - // /rustc/6cf088810f66fff15d05bf7135c5f5888b7c93b4/library/core/src/ops/ - // function.rs:250:5 note: Some details are omitted, run with - // `RUST_BACKTRACE=full` for a verbose backtrace. "###); - // + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "base", "-d", "b"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Also rebased 3 descendant commits onto parent of rebased commit + Working copy now at: vruxwmqv a72f0141 c | c + Parent commit : royxmykx 7033e775 b | b + Added 0 files, modified 0 files, removed 1 files + "###); // Unsimlified ancestry would look like // @ c // │ ◉ base @@ -950,12 +893,11 @@ fn test_rebase_with_child_and_descendant_bug_2600() { // ├─╯ // ◉ insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" - @ c - ◉ b - ├─╮ - │ ◉ a - ├─╯ ◉ base + │ @ c + ├─╯ + ◉ b + ◉ a ◉ "###); @@ -965,7 +907,7 @@ fn test_rebase_with_child_and_descendant_bug_2600() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "base", "-d", "a"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 4 descendant commits onto parent of rebased commit + Also rebased 3 descendant commits onto parent of rebased commit Working copy now at: vruxwmqv 0b91d0eb c | c Parent commit : royxmykx fb944989 b | b Added 0 files, modified 0 files, removed 1 files