diff --git a/CHANGELOG.md b/CHANGELOG.md index a40609a23..1f28bfd91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* `jj rebase` now accepts a `--branch/-b ` argument, which can be used + instead of `-r` or `-s` to specify which commits to rebase. It will rebase the + whole branch, relative to the destination. + * The new `jj print` command prints the contents of a file in a revision. * `jj move` and `jj squash` now lets you limit the set of changes to move by diff --git a/src/commands.rs b/src/commands.rs index 9c1fcf1ec..c63a28476 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1384,21 +1384,42 @@ struct MergeArgs { message: Option, } -/// Move a revision to a different parent +/// Move revisions to a different parent /// -/// With `-s`, rebases the specified revision and its descendants onto the -/// destination. For example, `jj rebase -s B -d D` would transform your history +/// There are three different ways of specifying which revisions to rebase: +/// `-b` to rebase a whole branch, `-s` to rebase a revision and its +/// descendants, and `-r` to rebase a single commit. If none if them is +/// specified, it defaults to `-r @`. +/// +/// With `-b`, it rebases the whole branch containing the specified revision. +/// Unlike `-s` and `-r`, the `-b` mode takes the destination into account +/// when calculating the set of revisions to rebase. That set includes the +/// specified revision and all ancestors that are not also ancestors +/// of the destination. It also includes all descendants of those commits. For +/// example, `jj rebase -b B -d D` or `jj rebase -b C -d D` would transform +/// your history like this: +/// +/// D B' +/// | | +/// | C D +/// | | => | +/// | B | C' +/// |/ |/ +/// A A +/// +/// With `-s`, it rebases the specified revision and its descendants onto the +/// destination. For example, `jj rebase -s C -d D` would transform your history /// like this: /// /// D C' /// | | -/// | C B' +/// | C D /// | | => | -/// | B D -/// |/ | +/// | B | B +/// |/ |/ /// A A /// -/// With `-r`, rebases only the specified revision onto the destination. Any +/// With `-r`, it rebases only the specified revision 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 B -d D` would transform /// your history like this: @@ -1412,15 +1433,18 @@ struct MergeArgs { /// A A #[derive(clap::Args, Clone, Debug)] #[clap(verbatim_doc_comment)] -#[clap(group(ArgGroup::new("to_rebase").args(&["revision", "source"])))] +#[clap(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))] struct RebaseArgs { + /// Rebase the whole branch (relative to destination's ancestors) + #[clap(long, short)] + branch: Option, + /// Rebase this revision and its descendants + #[clap(long, short)] + source: Option, /// Rebase only this revision, rebasing descendants onto this revision's /// parent(s) #[clap(long, short)] revision: Option, - /// Rebase this revision and its descendants - #[clap(long, short)] - source: Option, /// The revision to rebase onto #[clap(long, short, required = true)] destination: Vec, @@ -3611,9 +3635,9 @@ fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs) -> Result let destination = workspace_command.resolve_single_rev(ui, revision_str)?; new_parents.push(destination); } - // TODO: Unless we want to allow both --revision and --source, is it better to - // replace --source by --rebase-descendants? - if let Some(source_str) = &args.source { + if let Some(branch_str) = &args.branch { + rebase_branch(ui, &mut workspace_command, &new_parents, branch_str)?; + } else if let Some(source_str) = &args.source { rebase_descendants(ui, &mut workspace_command, &new_parents, source_str)?; } else { let rev_str = args.revision.as_deref().unwrap_or("@"); @@ -3622,6 +3646,46 @@ fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs) -> Result Ok(()) } +fn rebase_branch( + ui: &mut Ui, + workspace_command: &mut WorkspaceCommandHelper, + new_parents: &[Commit], + branch_str: &str, +) -> Result<(), CommandError> { + let branch_commit = workspace_command.resolve_single_rev(ui, branch_str)?; + let mut tx = workspace_command + .start_transaction(&format!("rebase branch at {}", branch_commit.id().hex())); + check_rebase_destinations(workspace_command, new_parents, &branch_commit)?; + + let parent_ids = new_parents + .iter() + .map(|commit| commit.id().clone()) + .collect_vec(); + let roots_expression = RevsetExpression::commits(parent_ids) + .range(&RevsetExpression::commit(branch_commit.id().clone())) + .roots(); + let mut num_rebased = 0; + let store = workspace_command.repo.store(); + for root_result in roots_expression + .evaluate( + workspace_command.repo().as_repo_ref(), + Some(&workspace_command.workspace_id()), + ) + .unwrap() + .iter() + .commits(store) + { + let root_commit = root_result?; + workspace_command.check_rewriteable(&root_commit)?; + rebase_commit(ui.settings(), tx.mut_repo(), &root_commit, new_parents); + num_rebased += 1; + } + num_rebased += tx.mut_repo().rebase_descendants(ui.settings()); + writeln!(ui, "Rebased {} commits", num_rebased)?; + workspace_command.finish_transaction(ui, tx)?; + Ok(()) +} + fn rebase_descendants( ui: &mut Ui, workspace_command: &mut WorkspaceCommandHelper, diff --git a/tests/test_rebase_command.rs b/tests/test_rebase_command.rs index d03a0a0e9..3054d8f24 100644 --- a/tests/test_rebase_command.rs +++ b/tests/test_rebase_command.rs @@ -58,6 +58,10 @@ fn test_rebase_invalid() { let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-r", "a", "-s", "a", "-d", "b"]); insta::assert_snapshot!(stderr.lines().next().unwrap(), @"error: The argument '--revision ' cannot be used with '--source '"); + // Both -b and -s + let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-b", "a", "-s", "a", "-d", "b"]); + insta::assert_snapshot!(stderr.lines().next().unwrap(), @"error: The argument '--branch ' cannot be used with '--source '"); + // Rebase onto descendant with -r let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-r", "a", "-d", "b"]); insta::assert_snapshot!(stderr, @"Error: Cannot rebase 247da0ddee3d onto descendant 18db23c14b3c @@ -69,6 +73,110 @@ fn test_rebase_invalid() { "); } +#[test] +fn test_rebase_branch() { + 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"); + + 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", &["b"]); + create_commit(&test_env, &repo_path, "e", &["a"]); + // Test the setup + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches"]); + insta::assert_snapshot!(stdout, @r###" + @ + o e + | o d + | | o c + | |/ + | o b + |/ + o a + o + "###); + + let stdout = test_env.jj_cmd_success(&repo_path, &["rebase", "-b", "c", "-d", "e"]); + insta::assert_snapshot!(stdout, @"Rebased 3 commits +"); + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches"]); + insta::assert_snapshot!(stdout, @r###" + o d + | o c + |/ + o b + | @ + |/ + o e + o a + o + "###); +} + +#[test] +fn test_rebase_branch_with_merge() { + 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"); + + create_commit(&test_env, &repo_path, "a", &[]); + create_commit(&test_env, &repo_path, "b", &["a"]); + create_commit(&test_env, &repo_path, "c", &[]); + create_commit(&test_env, &repo_path, "d", &["c"]); + create_commit(&test_env, &repo_path, "e", &["a", "d"]); + // Test the setup + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches"]); + insta::assert_snapshot!(stdout, @r###" + @ + o e + |\ + o | d + o | c + | | o b + | |/ + | o a + |/ + o + "###); + + let stdout = test_env.jj_cmd_success(&repo_path, &["rebase", "-b", "d", "-d", "b"]); + insta::assert_snapshot!(stdout, @r###" + Rebased 4 commits + Working copy now at: f6eecf0d8f36 + Added 1 files, modified 0 files, removed 0 files + "###); + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches"]); + insta::assert_snapshot!(stdout, @r###" + @ + o e + o d + o c + o b + o a + o + "###); + + test_env.jj_cmd_success(&repo_path, &["undo"]); + let stdout = test_env.jj_cmd_success(&repo_path, &["rebase", "-b", "e", "-d", "b"]); + insta::assert_snapshot!(stdout, @r###" + Rebased 4 commits + Working copy now at: a15dfb947f3f + Added 1 files, modified 0 files, removed 0 files + "###); + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches"]); + insta::assert_snapshot!(stdout, @r###" + @ + o e + o d + o c + o b + o a + o + "###); +} + #[test] fn test_rebase_single_revision() { let test_env = TestEnvironment::default();