diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae03aa2f0..e705df7081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The default template alias `builtin_log_root(change_id: ChangeId, commit_id: CommitId)` was replaced by `format_root_commit(root: Commit)`. +* The `--revision` option of `jj rebase` is renamed to `--revisions`. The short + alias `-r` is still supported. + ### New features * The list of conflicted paths is printed whenever the working copy changes. @@ -48,6 +51,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * You can check whether Watchman fsmonitor is enabled or installed with the new `jj debug watchman status` command. +* `jj rebase` now accepts revsets resolving to multiple revisions with the + `--revisions`/`-r` option. + ### Fixed bugs * Revsets now support `\`-escapes in string literal. diff --git a/cli/src/commands/rebase.rs b/cli/src/commands/rebase.rs index 40764dfb16..78de8efed5 100644 --- a/cli/src/commands/rebase.rs +++ b/cli/src/commands/rebase.rs @@ -20,13 +20,12 @@ 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}; use jj_lib::revset::{RevsetExpression, RevsetIteratorExt}; -use jj_lib::rewrite::{ - rebase_commit, rebase_commit_with_options, CommitRewriter, EmptyBehaviour, RebaseOptions, -}; +use jj_lib::rewrite::{rebase_commit_with_options, CommitRewriter, EmptyBehaviour, RebaseOptions}; use jj_lib::settings::UserSettings; use tracing::instrument; @@ -91,7 +90,7 @@ use crate::ui::Ui; /// J J /// ``` /// -/// With `-r`, the command rebases only the specified revision onto the +/// With `-r`, the command rebases only the specified revisions onto the /// destination. Any "hole" left behind will be filled by rebasing descendants /// onto the specified revision's parent(s). For example, `jj rebase -r K -d M` /// would transform your history like this: @@ -125,7 +124,7 @@ use crate::ui::Ui; /// commit. This is true in general; it is not specific to this command. #[derive(clap::Args, Clone, Debug)] #[command(verbatim_doc_comment)] -#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))] +#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revisions"])))] pub(crate) struct RebaseArgs { /// Rebase the whole branch relative to destination's ancestors (can be /// repeated) @@ -147,7 +146,7 @@ pub(crate) struct RebaseArgs { /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. #[arg(long, short)] source: Vec, - /// Rebase only this revision, rebasing descendants onto this revision's + /// Rebase the given revisions, rebasing descendants onto this revision's /// parent(s) /// /// Unlike `-s` or `-b`, you may `jj rebase -r` a revision `A` onto a @@ -155,7 +154,7 @@ pub(crate) struct RebaseArgs { /// /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. #[arg(long, short)] - revision: Option, + revisions: Vec, /// The revision(s) to rebase onto (can be repeated to create a merge /// commit) #[arg(long, short, required = true)] @@ -165,7 +164,7 @@ pub(crate) struct RebaseArgs { /// abandoned. It will not be abandoned if it was already empty before the /// rebase. Will never skip merge commits with multiple non-empty /// parents. - #[arg(long, conflicts_with = "revision")] + #[arg(long, conflicts_with = "revisions")] skip_empty: bool, /// Deprecated. Please prefix the revset with `all:` instead. @@ -198,7 +197,7 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets .resolve_some_revsets_default_single(&args.destination)? .into_iter() .collect_vec(); - if let Some(rev_arg) = &args.revision { + if !args.revisions.is_empty() { assert_eq!( // In principle, `-r --skip-empty` could mean to abandon the `-r` // commit if it becomes empty. This seems internally consistent with @@ -214,12 +213,16 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets EmptyBehaviour::Keep, "clap should forbid `-r --skip-empty`" ); - rebase_revision( + let revision_commits: Vec<_> = workspace_command + .parse_union_revsets(&args.revisions)? + .evaluate_to_commits()? + .try_collect()?; // in reverse topological order + rebase_revisions( ui, command.settings(), &mut workspace_command, &new_parents, - rev_arg, + revision_commits, )?; } else if !args.source.is_empty() { let source_commits = workspace_command.resolve_some_revsets_default_single(&args.source)?; @@ -348,91 +351,108 @@ fn rebase_descendants_transaction( Ok(()) } -fn rebase_revision( +fn rebase_revisions( ui: &mut Ui, settings: &UserSettings, workspace_command: &mut WorkspaceCommandHelper, new_parents: &[Commit], - rev_arg: &RevisionArg, + target_commits: Vec, ) -> Result<(), CommandError> { - 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(to_rebase_commit.id()), - ))); + if target_commits.is_empty() { + return Ok(()); + } + + workspace_command.check_rewritable(target_commits.iter().ids())?; + for commit in target_commits.iter() { + if new_parents.contains(commit) { + return Err(user_error(format!( + "Cannot rebase {} onto itself", + short_commit_hash(commit.id()), + ))); + } } let mut tx = workspace_command.start_transaction(); - let mut rebased_commit_ids = HashMap::new(); + let tx_description = if target_commits.len() == 1 { + format!("rebase commit {}", target_commits[0].id().hex()) + } else { + format!( + "rebase commit {} and {} more", + target_commits[0].id().hex(), + target_commits.len() - 1 + ) + }; + + // First, rebase the descendants of `target_commits`. + let (initial_rebased_commit_ids, target_roots) = + extract_commits(&mut tx, settings, &target_commits)?; + + // We now update `new_parents` to account for the rebase of all of + // `target_commits`'s descendants. Even if some of the original `new_parents` + // were descendants of `target_commits`, this will no longer be the case after + // the update. + let new_parents = new_parents + .iter() + .map(|new_parent| { + get_possibly_rewritten_commit_id(&initial_rebased_commit_ids, new_parent.id()) + }) + .collect_vec(); + let mut new_rebased_commit_ids = HashMap::new(); + let mut skipped_commits = Vec::new(); - // 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. + // At this point, all commits in the target set will only have other commits in + // the set as their ancestors. We can now safely rebase `target_commits` onto + // the `new_parents`, by updating the roots' parents and rebasing its + // descendants. tx.mut_repo().transform_descendants( settings, - vec![to_rebase_commit.id().clone()], + target_roots.iter().cloned().collect_vec(), |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. - rewriter.replace_parent(to_rebase_commit.id(), to_rebase_commit.parent_ids()); + if target_roots.contains(&old_commit_id) { + rewriter.set_new_parents(new_parents.clone()); + } if rewriter.parents_changed() { let builder = rewriter.rebase(settings)?; let commit = builder.write()?; - rebased_commit_ids.insert(old_commit_id, commit.id().clone()); + new_rebased_commit_ids.insert(old_commit_id, commit.id().clone()); + } else { + skipped_commits.push(rewriter.old_commit().clone()); } Ok(()) }, )?; - let num_rebased_descendants = rebased_commit_ids.len(); - - // We now update `new_parents` to account for the rebase of all of - // `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. - let new_parents = new_parents - .iter() - .map(|new_parent| { - rebased_commit_ids - .get(new_parent.id()) - .unwrap_or(new_parent.id()) - .clone() - }) - .collect(); - - 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 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(), &to_rebase_commit)?; - writeln!(formatter)?; + if let Some(mut fmt) = ui.status_formatter() { + if !skipped_commits.is_empty() { + if skipped_commits.len() == 1 { + write!(fmt, "Skipping rebase of commit ")?; + tx.write_commit_summary(fmt.as_mut(), &skipped_commits[0])?; + writeln!(fmt)?; + } else { + writeln!(fmt, "Skipping rebase of commits:")?; + for commit in skipped_commits.iter() { + write!(fmt, " ")?; + tx.write_commit_summary(fmt.as_mut(), commit)?; + writeln!(fmt)?; + } + } } - true - } else { - rebase_commit(settings, tx.mut_repo(), to_rebase_commit, new_parents)?; - debug_assert_eq!(tx.mut_repo().rebase_descendants(settings)?, 0); - false - }; - - if num_rebased_descendants > 0 { - if skipped_commit_rebase { - writeln!( - ui.status(), - "Rebased {num_rebased_descendants} descendant commits onto parent of commit" - )?; - } else { + let num_rebased = target_commits.len() - skipped_commits.len(); + let num_rebased_descendants = initial_rebased_commit_ids + .iter() + .filter(|(_, new_commit_id)| !new_rebased_commit_ids.contains_key(new_commit_id)) + .count(); + if num_rebased > 0 { + writeln!(fmt, "Rebased {num_rebased} commits onto destination")?; + } + if num_rebased_descendants > 0 { writeln!( - ui.status(), - "Also rebased {num_rebased_descendants} descendant commits onto parent of rebased \ - commit" + fmt, + "Rebased {num_rebased_descendants} descendant commits onto parent of rebased \ + commits" )?; } } @@ -443,6 +463,160 @@ fn rebase_revision( } } +/// Extracts `target_commits` from the graph by rebasing its descendants onto +/// its parents. This assumes that `target_commits` can be rewritten. +/// `target_commits` should be in reverse topological order. +/// Returns a map from the original commit IDs to the rewritten commit IDs, and +/// the roots of the `target_commits` set, using the rewritten commit ID if +/// applicable. +fn extract_commits( + tx: &mut WorkspaceCommandTransaction, + settings: &UserSettings, + target_commits: &[Commit], +) -> Result<(HashMap, IndexSet), CommandError> { + let repo = tx.mut_repo(); + let connected_target_commits: Vec<_> = + RevsetExpression::commits(target_commits.iter().ids().cloned().collect_vec()) + .connected() + .evaluate_programmatic(repo)? + .iter() + .commits(repo.store()) + .try_collect()?; + + let target_commit_ids: IndexSet<_> = target_commits.iter().ids().cloned().collect(); + + // Construct a list of parents for each commit in the connected graph of the + // target set which is not in the target set, which consists of ancestors which + // are in the target set. + let mut connected_target_parents: HashMap> = HashMap::new(); + for commit in connected_target_commits.iter().rev() { + if target_commit_ids.contains(commit.id()) { + continue; + } + let mut new_parents = IndexSet::new(); + for old_parent in commit.parent_ids() { + if let Some(parents) = connected_target_parents.get(old_parent) { + new_parents.extend(parents.iter().cloned()); + } else if target_commit_ids.contains(old_parent) { + new_parents.insert(old_parent.clone()); + } + } + connected_target_parents.insert(commit.id().clone(), new_parents); + } + + // Commits in the target set should only have other commits in the set as + // parents, except the roots of the set, which persist their original + // parents. + let mut new_target_parents: HashMap> = HashMap::new(); + for commit in target_commits.iter().rev() { + // The roots of the set will not have any parents found in `new_target_parents`, + // and will be stored in `new_target_parents` as an empty vector. + let mut new_parents = vec![]; + for old_parent in commit.parent_ids() { + if new_target_parents.contains_key(old_parent) { + new_parents.push(old_parent.clone()); + } else if let Some(parents) = connected_target_parents.get(old_parent) { + new_parents.extend(parents.iter().cloned()); + } + } + new_target_parents.insert(commit.id().clone(), new_parents); + } + + // If a commit outside the target set has a commit in the target set as a + // parent, then - after the transformation - it should have that commit's + // ancestors which are not in the target set as parents. + 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()); + } else { + new_parents.insert(old_parent.clone()); + } + } + new_child_parents.insert(commit.id().clone(), new_parents); + } + + let mut rebased_commit_ids = HashMap::new(); + + // TODO(ilyagr): Consider making it possible for these descendants + // to become emptied, like --skip-empty. This would require writing careful + // tests. + tx.mut_repo().transform_descendants( + settings, + target_commits.iter().ids().cloned().collect_vec(), + |mut rewriter| { + let old_commit = rewriter.old_commit(); + let old_commit_id = old_commit.id().clone(); + + // Commits in the target set should persist only rebased parents from the target + // sets. + if let Some(new_parents) = new_target_parents.get(&old_commit_id) { + // If the commit does not have any parents in the target set, its parents + // will be persisted since it is one of the roots of the set. + if !new_parents.is_empty() { + rewriter.set_new_rewritten_parents(new_parents.clone()); + } + } + // Commits outside the target set should have references to commits inside the set + // replaced. + 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()); + } + } + rewriter.set_new_rewritten_parents(new_parents); + } + if rewriter.parents_changed() { + let builder = rewriter.rebase(settings)?; + let commit = builder.write()?; + rebased_commit_ids.insert(old_commit_id, commit.id().clone()); + } + Ok(()) + }, + )?; + + // Compute the roots of `target_commits`, and update them to account for the + // rebase of all of `target_commits`'s descendants. + let target_roots: IndexSet<_> = new_target_parents + .iter() + .filter_map(|(commit_id, parents)| { + if parents.is_empty() { + Some(get_possibly_rewritten_commit_id( + &rebased_commit_ids, + commit_id, + )) + } else { + None + } + }) + .collect(); + + Ok((rebased_commit_ids, target_roots)) +} + +/// Returns either the `commit_id` if the commit was not rewritten, or the +/// ID of the rewritten commit corresponding to `commit_id`. +fn get_possibly_rewritten_commit_id( + rewritten_commit_ids: &HashMap, + commit_id: &CommitId, +) -> CommitId { + rewritten_commit_ids + .get(commit_id) + .unwrap_or(commit_id) + .clone() +} + fn check_rebase_destinations( repo: &Arc, new_parents: &[Commit], diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 6510c3c4ef..83747368af 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1495,7 +1495,7 @@ O N' J J ``` -With `-r`, the command rebases only the specified revision onto the +With `-r`, the command rebases only the specified revisions onto the destination. Any "hole" left behind will be filled by rebasing descendants onto the specified revision's parent(s). For example, `jj rebase -r K -d M` would transform your history like this: @@ -1534,7 +1534,7 @@ commit. This is true in general; it is not specific to this command. * `-b`, `--branch ` — Rebase the whole branch relative to destination's ancestors (can be repeated) * `-s`, `--source ` — Rebase specified revision(s) together with their trees of descendants (can be repeated) -* `-r`, `--revision ` — Rebase only this revision, rebasing descendants onto this revision's parent(s) +* `-r`, `--revisions ` — Rebase the given revisions, rebasing descendants onto this revision's parent(s) * `-d`, `--destination ` — The revision(s) to rebase onto (can be repeated to create a merge commit) * `--skip-empty` — If true, when rebasing would produce an empty commit, the commit is abandoned. It will not be abandoned if it was already empty before the rebase. Will never skip merge commits with multiple non-empty parents diff --git a/cli/tests/test_rebase_command.rs b/cli/tests/test_rebase_command.rs index f137983db5..7876ab456c 100644 --- a/cli/tests/test_rebase_command.rs +++ b/cli/tests/test_rebase_command.rs @@ -52,9 +52,9 @@ fn test_rebase_invalid() { let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase", "-r", "a", "-s", "a", "-d", "b"]); insta::assert_snapshot!(stderr, @r###" - error: the argument '--revision ' cannot be used with '--source ' + error: the argument '--revisions ' cannot be used with '--source ' - Usage: jj rebase --destination --revision + Usage: jj rebase --destination --revisions For more information, try '--help'. "###); @@ -76,9 +76,9 @@ fn test_rebase_invalid() { &["rebase", "-r", "a", "-d", "b", "--skip-empty"], ); insta::assert_snapshot!(stderr, @r###" - error: the argument '--revision ' cannot be used with '--skip-empty' + error: the argument '--revisions ' cannot be used with '--skip-empty' - Usage: jj rebase --destination --revision + Usage: jj rebase --destination --revisions For more information, try '--help'. "###); @@ -287,7 +287,8 @@ fn test_rebase_single_revision() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "-d", "b"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 2 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 2 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq 2668ffbe e | e Parent commit : vruxwmqv 7b370c85 d | d Added 0 files, modified 0 files, removed 1 files @@ -309,7 +310,8 @@ fn test_rebase_single_revision() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "d", "-d", "a"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 1 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 1 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq ed210c15 e | e Parent commit : zsuskuln 1394f625 b | b Parent commit : royxmykx c0cb3a0b c | c @@ -354,7 +356,8 @@ fn test_rebase_single_revision_merge_parent() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "-d", "a"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 1 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 1 descendant commits onto parent of rebased commits Working copy now at: vruxwmqv a37531e8 d | d Parent commit : rlvkpnrz 2443ea76 a | a Parent commit : zsuskuln d370aee1 b | b @@ -371,6 +374,191 @@ fn test_rebase_single_revision_merge_parent() { "###); } +#[test] +fn test_rebase_multiple_revisions() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + create_commit(&test_env, &repo_path, "a", &[]); + create_commit(&test_env, &repo_path, "b", &["a"]); + create_commit(&test_env, &repo_path, "c", &["b"]); + create_commit(&test_env, &repo_path, "d", &["a"]); + create_commit(&test_env, &repo_path, "e", &["d"]); + create_commit(&test_env, &repo_path, "f", &["c", "e"]); + create_commit(&test_env, &repo_path, "g", &["f"]); + create_commit(&test_env, &repo_path, "h", &["g"]); + create_commit(&test_env, &repo_path, "i", &["f"]); + // Test the setup + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + @ i + │ ◉ h + │ ◉ g + ├─╯ + ◉ f + ├─╮ + │ ◉ e + │ ◉ d + ◉ │ c + ◉ │ b + ├─╯ + ◉ a + ◉ + "###); + + // Test with two non-related non-merge commits. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "-r", "e", "-d", "a"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 2 commits onto destination + Rebased 4 descendant commits onto parent of rebased commits + Working copy now at: xznxytkn 016685dc i | i + Parent commit : kmkuslsw e04d3932 f | f + Added 0 files, modified 0 files, removed 2 files + "###); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + ◉ e + │ ◉ c + ├─╯ + │ @ i + │ │ ◉ h + │ │ ◉ g + │ ├─╯ + │ ◉ f + │ ├─╮ + │ │ ◉ d + ├───╯ + │ ◉ b + ├─╯ + ◉ a + ◉ + "###); + test_env.jj_cmd_ok(&repo_path, &["undo"]); + + // Test with two related non-merge commits. Since "b" is a parent of "c", when + // rebasing commits "b" and "c", their ancestry relationship should be + // preserved. + let (stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "b", "-r", "c", "-d", "e"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 2 commits onto destination + Rebased 4 descendant commits onto parent of rebased commits + Working copy now at: xznxytkn 94538385 i | i + Parent commit : kmkuslsw dae8d293 f | f + Added 0 files, modified 0 files, removed 2 files + "###); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + ◉ c + ◉ b + │ @ i + │ │ ◉ h + │ │ ◉ g + │ ├─╯ + │ ◉ f + ╭─┤ + ◉ │ e + ◉ │ d + ├─╯ + ◉ a + ◉ + "###); + test_env.jj_cmd_ok(&repo_path, &["undo"]); + + // Test with a subgraph containing a merge commit. Since the merge commit "f" + // was extracted, its descendants which are not part of the subgraph will + // inherit its descendants which are not in the subtree ("c" and "d"). + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "e::g", "-d", "a"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 3 commits onto destination + Rebased 2 descendant commits onto parent of rebased commits + Working copy now at: xznxytkn 1868ded4 i | i + Parent commit : royxmykx 7e4fbf4f c | c + Parent commit : vruxwmqv 4cc44fbf d | d + Added 0 files, modified 0 files, removed 2 files + "###); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + ◉ g + ◉ f + ◉ e + │ @ i + │ ├─╮ + │ │ │ ◉ h + │ ╭─┬─╯ + │ │ ◉ d + ├───╯ + │ ◉ c + │ ◉ b + ├─╯ + ◉ a + ◉ + "###); + test_env.jj_cmd_ok(&repo_path, &["undo"]); + + // Test with commits in a disconnected subgraph. The subgraph has the + // relationship d->e->f->g->h, but only "d", "f" and "h" are in the set of + // rebased commits. "d" should be a new parent of "f", and "f" should be a + // new parent of "g". + let (stdout, stderr) = test_env.jj_cmd_ok( + &repo_path, + &["rebase", "-r", "d", "-r", "f", "-r", "h", "-d", "b"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 3 commits onto destination + Rebased 3 descendant commits onto parent of rebased commits + Working copy now at: xznxytkn 9cfd1635 i | i + Parent commit : royxmykx 7e4fbf4f c | c + Parent commit : znkkpsqq ecf9a1d5 e | e + Added 0 files, modified 0 files, removed 2 files + "###); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + ◉ h + ◉ f + ◉ d + │ @ i + │ ├─╮ + │ │ │ ◉ g + │ ╭─┬─╯ + │ │ ◉ e + │ ◉ │ c + ├─╯ │ + ◉ │ b + ├───╯ + ◉ a + ◉ + "###); + test_env.jj_cmd_ok(&repo_path, &["undo"]); + + // Test rebasing a subgraph onto its descendants. + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "d::e", "-d", "i"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 2 commits onto destination + Rebased 4 descendant commits onto parent of rebased commits + Working copy now at: xznxytkn 5d911e5c i | i + Parent commit : kmkuslsw d1bfda8c f | f + Added 0 files, modified 0 files, removed 2 files + "###); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + ◉ e + ◉ d + @ i + │ ◉ h + │ ◉ g + ├─╯ + ◉ f + ├─╮ + ◉ │ c + ◉ │ b + ├─╯ + ◉ a + ◉ + "###); +} + #[test] fn test_rebase_revision_onto_descendant() { let test_env = TestEnvironment::default(); @@ -397,7 +585,8 @@ fn test_rebase_revision_onto_descendant() { 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 3 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 3 descendant commits onto parent of rebased commits Working copy now at: vruxwmqv bff4a4eb merge | merge Parent commit : royxmykx c84e900d b | b Parent commit : zsuskuln d57db87b a | a @@ -425,7 +614,8 @@ fn test_rebase_revision_onto_descendant() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "base", "-d", "merge"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 3 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 3 descendant commits onto parent of rebased commits Working copy now at: vruxwmqv 986b7a49 merge | merge Parent commit : royxmykx c07c677c b | b Parent commit : zsuskuln abc90087 a | a @@ -467,7 +657,9 @@ fn test_rebase_multiple_destinations() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "a", "-d", "b", "-d", "c"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 1 commits onto destination + "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ a ├─╮ @@ -489,7 +681,9 @@ fn test_rebase_multiple_destinations() { // try with 'all:' and succeed let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "a", "-d", "all:b|c"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + Rebased 1 commits onto destination + "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ a ├─╮ @@ -859,7 +1053,8 @@ fn test_rebase_with_child_and_descendant_bug_2600() { test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "base", "-d", "root()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 3 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 3 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq 45371aaf c | c Parent commit : vruxwmqv c0a76bf4 b | b Added 0 files, modified 0 files, removed 1 files @@ -883,7 +1078,8 @@ fn test_rebase_with_child_and_descendant_bug_2600() { 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 + Rebased 1 commits onto destination + Rebased 3 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq e28fa972 c | c Parent commit : vruxwmqv 8d0eeb6a b | b Added 0 files, modified 0 files, removed 1 files @@ -906,7 +1102,8 @@ 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 3 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 3 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq a9da974c c | c Parent commit : vruxwmqv 0072139c b | b Added 0 files, modified 0 files, removed 1 files @@ -938,7 +1135,8 @@ fn test_rebase_with_child_and_descendant_bug_2600() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "a", "-d", "root()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 2 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 2 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq 7210b05e c | c Parent commit : vruxwmqv da3f7511 b | b Added 0 files, modified 0 files, removed 1 files @@ -959,7 +1157,8 @@ fn test_rebase_with_child_and_descendant_bug_2600() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "b", "-d", "root()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 1 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 1 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq f280545e c | c Parent commit : zsuskuln 0a7fb8f6 base | base Parent commit : royxmykx 86a06598 a | a @@ -984,7 +1183,8 @@ fn test_rebase_with_child_and_descendant_bug_2600() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "b", "-d", "c"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 1 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 1 descendant commits onto parent of rebased commits Working copy now at: znkkpsqq c0a7cd80 c | c Parent commit : zsuskuln 0a7fb8f6 base | base Parent commit : royxmykx 86a06598 a | a @@ -1007,6 +1207,7 @@ fn test_rebase_with_child_and_descendant_bug_2600() { let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "-d", "a"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + Rebased 1 commits onto destination Working copy now at: znkkpsqq 7a3bc050 c | c Parent commit : royxmykx 86a06598 a | a Added 0 files, modified 0 files, removed 1 files @@ -1162,7 +1363,7 @@ fn test_rebase_skip_if_on_destination() { // Skip rebase of commit, but rebases children onto destination with -r insta::assert_snapshot!(stderr, @r###" Skipping rebase of commit kmkuslsw 48dd9e3f e | e - Rebased 1 descendant commits onto parent of commit + Rebased 1 descendant commits onto parent of rebased commits Working copy now at: lylxulpl 77cb229f f | f Parent commit : vruxwmqv c41e416e c | c Added 0 files, modified 0 files, removed 1 files diff --git a/cli/tests/test_repo_change_report.rs b/cli/tests/test_repo_change_report.rs index 41cfa660f6..ae32742c32 100644 --- a/cli/tests/test_repo_change_report.rs +++ b/cli/tests/test_repo_change_report.rs @@ -64,7 +64,8 @@ fn test_report_conflicts() { test_env.jj_cmd_ok(&repo_path, &["rebase", "-r=description(B)", "-d=root()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" - Also rebased 2 descendant commits onto parent of rebased commit + Rebased 1 commits onto destination + Rebased 2 descendant commits onto parent of rebased commits New conflicts appeared in these commits: rlvkpnrz 262c4c38 (conflict) B kkmpptxz d1edf578 (conflict) C