forked from mirrors/jj
cli: add mode for rebasing branch onto destination (#168)
This commit is contained in:
parent
a6d0f5fe21
commit
30f5471fc3
3 changed files with 190 additions and 14 deletions
|
@ -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 <revision>` 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
|
||||
|
|
|
@ -1384,21 +1384,42 @@ struct MergeArgs {
|
|||
message: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
/// Rebase this revision and its descendants
|
||||
#[clap(long, short)]
|
||||
source: Option<String>,
|
||||
/// Rebase only this revision, rebasing descendants onto this revision's
|
||||
/// parent(s)
|
||||
#[clap(long, short)]
|
||||
revision: Option<String>,
|
||||
/// Rebase this revision and its descendants
|
||||
#[clap(long, short)]
|
||||
source: Option<String>,
|
||||
/// The revision to rebase onto
|
||||
#[clap(long, short, required = true)]
|
||||
destination: Vec<String>,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 <REVISION>' cannot be used with '--source <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 <BRANCH>' cannot be used with '--source <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();
|
||||
|
|
Loading…
Reference in a new issue