ok/jj
1
0
Fork 0
forked from mirrors/jj

cli: add mode for rebasing branch onto destination (#168)

This commit is contained in:
Martin von Zweigbergk 2022-04-13 10:53:50 -07:00 committed by Martin von Zweigbergk
parent a6d0f5fe21
commit 30f5471fc3
3 changed files with 190 additions and 14 deletions

View file

@ -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

View file

@ -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,

View file

@ -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();