rebase: add --insert-after and --insert-before options for --revisions

This commit is contained in:
Benjamin Tan 2024-03-29 21:32:55 +08:00
parent 714bc0a9e6
commit f75461efc1
5 changed files with 1036 additions and 20 deletions

View file

@ -62,6 +62,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `jj rebase` now accepts revsets resolving to multiple revisions with the * `jj rebase` now accepts revsets resolving to multiple revisions with the
`--revisions`/`-r` option. `--revisions`/`-r` option.
* `jj rebase -r` now accepts `--insert-after` and `--insert-before` options to
customize the location of the rebased revisions.
### Fixed bugs ### Fixed bugs
* Revsets now support `\`-escapes in string literal. * Revsets now support `\`-escapes in string literal.

View file

@ -15,6 +15,7 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::io::Write; use std::io::Write;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use clap::ArgGroup; use clap::ArgGroup;
@ -126,6 +127,7 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)] #[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)] #[command(verbatim_doc_comment)]
#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revisions"])))] #[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revisions"])))]
#[command(group(ArgGroup::new("target").args(&["destination", "insert_after", "insert_before"]).required(true)))]
pub(crate) struct RebaseArgs { pub(crate) struct RebaseArgs {
/// Rebase the whole branch relative to destination's ancestors (can be /// Rebase the whole branch relative to destination's ancestors (can be
/// repeated) /// repeated)
@ -158,8 +160,34 @@ pub(crate) struct RebaseArgs {
revisions: Vec<RevisionArg>, revisions: Vec<RevisionArg>,
/// The revision(s) to rebase onto (can be repeated to create a merge /// The revision(s) to rebase onto (can be repeated to create a merge
/// commit) /// commit)
#[arg(long, short, required = true)] #[arg(long, short)]
destination: Vec<RevisionArg>, destination: Vec<RevisionArg>,
/// The revision(s) to insert after (can be repeated to create a merge
/// commit)
///
/// Only works with `-r`.
#[arg(
long,
short = 'A',
visible_alias = "after",
conflicts_with = "destination",
conflicts_with = "source",
conflicts_with = "branch"
)]
insert_after: Vec<RevisionArg>,
/// The revision(s) to insert before (can be repeated to create a merge
/// commit)
///
/// Only works with `-r`.
#[arg(
long,
short = 'B',
visible_alias = "before",
conflicts_with = "destination",
conflicts_with = "source",
conflicts_with = "branch"
)]
insert_before: Vec<RevisionArg>,
/// If true, when rebasing would produce an empty commit, the commit is /// 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 /// abandoned. It will not be abandoned if it was already empty before the
@ -194,10 +222,6 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets
simplify_ancestor_merge: false, simplify_ancestor_merge: false,
}; };
let mut workspace_command = command.workspace_helper(ui)?; let mut workspace_command = command.workspace_helper(ui)?;
let new_parents = workspace_command
.resolve_some_revsets_default_single(&args.destination)?
.into_iter()
.collect_vec();
if !args.revisions.is_empty() { if !args.revisions.is_empty() {
assert_eq!( assert_eq!(
// In principle, `-r --skip-empty` could mean to abandon the `-r` // In principle, `-r --skip-empty` could mean to abandon the `-r`
@ -218,6 +242,31 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets
.parse_union_revsets(&args.revisions)? .parse_union_revsets(&args.revisions)?
.evaluate_to_commits()? .evaluate_to_commits()?
.try_collect()?; // in reverse topological order .try_collect()?; // in reverse topological order
if !args.insert_after.is_empty() {
let after_commits =
workspace_command.resolve_some_revsets_default_single(&args.insert_after)?;
rebase_revisions_after(
ui,
command.settings(),
&mut workspace_command,
&after_commits,
&target_commits,
)?;
} else if !args.insert_before.is_empty() {
let before_commits =
workspace_command.resolve_some_revsets_default_single(&args.insert_before)?;
rebase_revisions_before(
ui,
command.settings(),
&mut workspace_command,
&before_commits,
&target_commits,
)?;
} else {
let new_parents = workspace_command
.resolve_some_revsets_default_single(&args.destination)?
.into_iter()
.collect_vec();
rebase_revisions( rebase_revisions(
ui, ui,
command.settings(), command.settings(),
@ -225,7 +274,12 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets
&new_parents, &new_parents,
&target_commits, &target_commits,
)?; )?;
}
} else if !args.source.is_empty() { } else if !args.source.is_empty() {
let new_parents = workspace_command
.resolve_some_revsets_default_single(&args.destination)?
.into_iter()
.collect_vec();
let source_commits = workspace_command.resolve_some_revsets_default_single(&args.source)?; let source_commits = workspace_command.resolve_some_revsets_default_single(&args.source)?;
rebase_descendants_transaction( rebase_descendants_transaction(
ui, ui,
@ -236,6 +290,10 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets
rebase_options, rebase_options,
)?; )?;
} else { } else {
let new_parents = workspace_command
.resolve_some_revsets_default_single(&args.destination)?
.into_iter()
.collect_vec();
let branch_commits = if args.branch.is_empty() { let branch_commits = if args.branch.is_empty() {
IndexSet::from([workspace_command.resolve_single_rev(&RevisionArg::AT)?]) IndexSet::from([workspace_command.resolve_single_rev(&RevisionArg::AT)?])
} else { } else {
@ -387,6 +445,82 @@ fn rebase_revisions(
) )
} }
fn rebase_revisions_after(
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &mut WorkspaceCommandHelper,
after_commits: &IndexSet<Commit>,
target_commits: &[Commit],
) -> Result<(), CommandError> {
workspace_command.check_rewritable(target_commits.iter().ids())?;
let after_commit_ids = after_commits.iter().ids().cloned().collect_vec();
let new_parents_expression = RevsetExpression::commits(after_commit_ids.clone());
let new_children_expression = new_parents_expression.children();
ensure_no_commit_loop(
workspace_command.repo().as_ref(),
&new_children_expression,
&new_parents_expression,
)?;
let new_parent_ids = after_commit_ids;
let new_children: Vec<_> = new_children_expression
.evaluate_programmatic(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;
workspace_command.check_rewritable(new_children.iter().ids())?;
move_commits_transaction(
ui,
settings,
workspace_command,
&new_parent_ids,
&new_children,
target_commits,
)
}
fn rebase_revisions_before(
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &mut WorkspaceCommandHelper,
before_commits: &IndexSet<Commit>,
target_commits: &[Commit],
) -> Result<(), CommandError> {
workspace_command.check_rewritable(target_commits.iter().ids())?;
let before_commit_ids = before_commits.iter().ids().cloned().collect_vec();
workspace_command.check_rewritable(&before_commit_ids)?;
let new_children_expression = RevsetExpression::commits(before_commit_ids);
let new_parents_expression = new_children_expression.parents();
ensure_no_commit_loop(
workspace_command.repo().as_ref(),
&new_children_expression,
&new_parents_expression,
)?;
// Not using `new_parents_expression` here to persist the order of parents
// specified in `before_commits`.
let new_parent_ids: IndexSet<_> = before_commits
.iter()
.flat_map(|commit| commit.parent_ids().iter().cloned().collect_vec())
.collect();
let new_parent_ids = new_parent_ids.into_iter().collect_vec();
let new_children = before_commits.iter().cloned().collect_vec();
move_commits_transaction(
ui,
settings,
workspace_command,
&new_parent_ids,
&new_children,
target_commits,
)
}
/// Wraps `move_commits` in a transaction. /// Wraps `move_commits` in a transaction.
fn move_commits_transaction( fn move_commits_transaction(
ui: &mut Ui, ui: &mut Ui,
@ -772,6 +906,28 @@ fn move_commits(
)) ))
} }
/// Ensure that there is no possible cycle between the potential children and
/// parents of rebased commits.
fn ensure_no_commit_loop(
repo: &ReadonlyRepo,
children_expression: &Rc<RevsetExpression>,
parents_expression: &Rc<RevsetExpression>,
) -> Result<(), CommandError> {
if let Some(commit_id) = children_expression
.dag_range_to(parents_expression)
.evaluate_programmatic(repo)?
.iter()
.next()
{
return Err(user_error(format!(
"Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
the rebased commits",
short_commit_hash(&commit_id),
)));
}
Ok(())
}
fn check_rebase_destinations( fn check_rebase_destinations(
repo: &Arc<ReadonlyRepo>, repo: &Arc<ReadonlyRepo>,
new_parents: &[Commit], new_parents: &[Commit],

View file

@ -1532,7 +1532,7 @@ J J
If a working-copy commit gets abandoned, it will be given a new, empty If a working-copy commit gets abandoned, it will be given a new, empty
commit. This is true in general; it is not specific to this command. commit. This is true in general; it is not specific to this command.
**Usage:** `jj rebase [OPTIONS] --destination <DESTINATION>` **Usage:** `jj rebase [OPTIONS] <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>`
###### **Options:** ###### **Options:**
@ -1540,6 +1540,8 @@ commit. This is true in general; it is not specific to this command.
* `-s`, `--source <SOURCE>` — Rebase specified revision(s) together with their trees of descendants (can be repeated) * `-s`, `--source <SOURCE>` — Rebase specified revision(s) together with their trees of descendants (can be repeated)
* `-r`, `--revisions <REVISIONS>` — Rebase the given revisions, rebasing descendants onto this revision's parent(s) * `-r`, `--revisions <REVISIONS>` — Rebase the given revisions, rebasing descendants onto this revision's parent(s)
* `-d`, `--destination <DESTINATION>` — The revision(s) to rebase onto (can be repeated to create a merge commit) * `-d`, `--destination <DESTINATION>` — The revision(s) to rebase onto (can be repeated to create a merge commit)
* `-A`, `--insert-after <INSERT_AFTER>` — The revision(s) to insert after (can be repeated to create a merge commit)
* `-B`, `--insert-before <INSERT_BEFORE>` — The revision(s) to insert before (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 * `--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
Possible values: `true`, `false` Possible values: `true`, `false`

View file

@ -41,9 +41,9 @@ fn test_rebase_invalid() {
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase"]); let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase"]);
insta::assert_snapshot!(stderr, @r###" insta::assert_snapshot!(stderr, @r###"
error: the following required arguments were not provided: error: the following required arguments were not provided:
--destination <DESTINATION> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
Usage: jj rebase --destination <DESTINATION> Usage: jj rebase <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'. For more information, try '--help'.
"###); "###);
@ -54,7 +54,7 @@ fn test_rebase_invalid() {
insta::assert_snapshot!(stderr, @r###" insta::assert_snapshot!(stderr, @r###"
error: the argument '--revisions <REVISIONS>' cannot be used with '--source <SOURCE>' error: the argument '--revisions <REVISIONS>' cannot be used with '--source <SOURCE>'
Usage: jj rebase --destination <DESTINATION> --revisions <REVISIONS> Usage: jj rebase --revisions <REVISIONS> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'. For more information, try '--help'.
"###); "###);
@ -65,7 +65,7 @@ fn test_rebase_invalid() {
insta::assert_snapshot!(stderr, @r###" insta::assert_snapshot!(stderr, @r###"
error: the argument '--branch <BRANCH>' cannot be used with '--source <SOURCE>' error: the argument '--branch <BRANCH>' cannot be used with '--source <SOURCE>'
Usage: jj rebase --destination <DESTINATION> --branch <BRANCH> Usage: jj rebase --branch <BRANCH> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'. For more information, try '--help'.
"###); "###);
@ -78,7 +78,73 @@ fn test_rebase_invalid() {
insta::assert_snapshot!(stderr, @r###" insta::assert_snapshot!(stderr, @r###"
error: the argument '--revisions <REVISIONS>' cannot be used with '--skip-empty' error: the argument '--revisions <REVISIONS>' cannot be used with '--skip-empty'
Usage: jj rebase --destination <DESTINATION> --revisions <REVISIONS> Usage: jj rebase --revisions <REVISIONS> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'.
"###);
// Both -d and --after
let stderr = test_env.jj_cmd_cli_error(
&repo_path,
&["rebase", "-r", "a", "-d", "b", "--after", "b"],
);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--destination <DESTINATION>' cannot be used with '--insert-after <INSERT_AFTER>'
Usage: jj rebase --revisions <REVISIONS> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'.
"###);
// -s with --after
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase", "-s", "a", "--after", "b"]);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--source <SOURCE>' cannot be used with '--insert-after <INSERT_AFTER>'
Usage: jj rebase --source <SOURCE> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'.
"###);
// -b with --after
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase", "-b", "a", "--after", "b"]);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--branch <BRANCH>' cannot be used with '--insert-after <INSERT_AFTER>'
Usage: jj rebase --branch <BRANCH> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'.
"###);
// Both -d and --before
let stderr = test_env.jj_cmd_cli_error(
&repo_path,
&["rebase", "-r", "a", "-d", "b", "--before", "b"],
);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--destination <DESTINATION>' cannot be used with '--insert-before <INSERT_BEFORE>'
Usage: jj rebase --revisions <REVISIONS> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'.
"###);
// -s with --before
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase", "-s", "a", "--before", "b"]);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--source <SOURCE>' cannot be used with '--insert-before <INSERT_BEFORE>'
Usage: jj rebase --source <SOURCE> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'.
"###);
// -b with --before
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["rebase", "-b", "a", "--before", "b"]);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--branch <BRANCH>' cannot be used with '--insert-before <INSERT_BEFORE>'
Usage: jj rebase --branch <BRANCH> <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
For more information, try '--help'. For more information, try '--help'.
"###); "###);
@ -1229,6 +1295,796 @@ fn test_rebase_with_child_and_descendant_bug_2600() {
"###); "###);
} }
#[test]
fn test_rebase_revisions_after() {
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, "b1", &["a"]);
create_commit(&test_env, &repo_path, "b2", &["b1"]);
create_commit(&test_env, &repo_path, "b3", &["a"]);
create_commit(&test_env, &repo_path, "b4", &["b3"]);
create_commit(&test_env, &repo_path, "c", &["b2", "b4"]);
create_commit(&test_env, &repo_path, "d", &["c"]);
create_commit(&test_env, &repo_path, "e", &["c"]);
create_commit(&test_env, &repo_path, "f", &["e"]);
// Test the setup
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e4a00798
e nkmrtpmo 858693f7
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
let setup_opid = test_env.current_operation_id(&repo_path);
// Rebasing a commit after its parents should be a no-op.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "c", "--after", "b2", "--after", "b4"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Skipping rebase of commits:
kmkuslsw cd86b3e4 c | c
lylxulpl 7d0512e5 d | d
nkmrtpmo 858693f7 e | e
xznxytkn e4a00798 f | f
Nothing changed.
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e4a00798
e nkmrtpmo 858693f7
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
// Rebasing a commit after itself should be a no-op.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "--after", "c"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Skipping rebase of commits:
kmkuslsw cd86b3e4 c | c
lylxulpl 7d0512e5 d | d
nkmrtpmo 858693f7 e | e
xznxytkn e4a00798 f | f
Nothing changed.
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e4a00798
e nkmrtpmo 858693f7
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
// Rebase a commit after another commit. "c" has parents "b2" and "b4", so its
// children "d" and "e" should be rebased onto "b2" and "b4" respectively.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "--after", "e"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 3 descendant commits
Working copy now at: xznxytkn e0e873c8 f | f
Parent commit : kmkuslsw 754793f3 c | c
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e0e873c8
c kmkuslsw 754793f3
e nkmrtpmo e0d7fb63
d lylxulpl 5e9cb58d
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit after a leaf commit.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "e", "--after", "f"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 1 descendant commits
Working copy now at: xznxytkn 9804b742 f | f
Parent commit : kmkuslsw cd86b3e4 c | c
Added 0 files, modified 0 files, removed 1 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 76ac6464
@ f xznxytkn 9804b742
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit after a commit in a branch of a merge commit.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--after", "b1"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 4 descendant commits
Working copy now at: xznxytkn 80c27408 f | f
Parent commit : zsuskuln 072d5ae1 b1 | b1
Added 0 files, modified 0 files, removed 5 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo cee7a197
d lylxulpl 1eb960ec
c kmkuslsw 305a7803
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 526481b4
@ f xznxytkn 80c27408
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit after the last commit in a branch of a merge commit.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--after", "b2"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 3 descendant commits
Working copy now at: xznxytkn ebbc24b1 f | f
Parent commit : royxmykx 2b8e1148 b2 | b2
Added 0 files, modified 0 files, removed 4 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 3162ac52
d lylxulpl 6f7f3b2a
c kmkuslsw d33f69f1
@ f xznxytkn ebbc24b1
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit after a commit with multiple children.
// "c" has two children "d" and "e", so the rebased commit "f" will inherit the
// two children.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--after", "c"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 2 descendant commits
Working copy now at: xznxytkn 8f8c91d3 f | f
Parent commit : kmkuslsw cd86b3e4 c | c
Added 0 files, modified 0 files, removed 1 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 03ade273
d lylxulpl 8bccbeda
@ f xznxytkn 8f8c91d3
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit after multiple commits.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "f", "--after", "e", "--after", "d"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Working copy now at: xznxytkn 7784e5a0 f | f
Parent commit : nkmrtpmo 858693f7 e | e
Parent commit : lylxulpl 7d0512e5 d | d
Added 1 files, modified 0 files, removed 0 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 7784e5a0
d lylxulpl 7d0512e5
e nkmrtpmo 858693f7
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase two unrelated commits.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "d", "-r", "e", "--after", "a"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 2 commits onto destination
Rebased 6 descendant commits
Working copy now at: xznxytkn 0b53613e f | f
Parent commit : kmkuslsw 193687bb c | c
Added 1 files, modified 0 files, removed 0 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 0b53613e
c kmkuslsw 193687bb
b4 znkkpsqq e8d0f57b
b3 vruxwmqv cb48344c
b2 royxmykx 535f779d
b1 zsuskuln 693186c0
e nkmrtpmo 2bb4e0b6
d lylxulpl 0b921a1c
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a subgraph with merge commit and two parents, which should preserve
// the merge.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "b2", "-r", "b4", "-r", "c", "--after", "f"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 3 commits onto destination
Rebased 3 descendant commits
Working copy now at: xznxytkn eaf1d6b8 f | f
Parent commit : nkmrtpmo 0d7e4ce9 e | e
Added 0 files, modified 0 files, removed 3 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
d lylxulpl 16060da9
c kmkuslsw ef5ead27
b4 znkkpsqq 9c884b94
b2 royxmykx bdfea21d
@ f xznxytkn eaf1d6b8
e nkmrtpmo 0d7e4ce9
b3 vruxwmqv 523e6a8b
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a subgraph with four commits after one of the commits itself.
let (stdout, stderr) =
test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "b1::d", "--after", "c"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 4 commits onto destination
Rebased 2 descendant commits
Working copy now at: xznxytkn 084e0629 f | f
Parent commit : nkmrtpmo 563d78c6 e | e
Added 1 files, modified 0 files, removed 0 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 084e0629
e nkmrtpmo 563d78c6
d lylxulpl e67ba5c9
c kmkuslsw 049aa109
b2 royxmykx 7af3d6cd
b1 zsuskuln cd84b343
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a subgraph with disconnected commits. Since "b2" is an ancestor of
// "e", "b2" should be a parent of "e" after the rebase.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "e", "-r", "b2", "--after", "d"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 2 commits onto destination
Rebased 3 descendant commits
Working copy now at: xznxytkn 4fb2bb60 f | f
Parent commit : kmkuslsw cebde86a c | c
Added 0 files, modified 0 files, removed 2 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 4fb2bb60
e nkmrtpmo 1ea93588
b2 royxmykx 064e3bcb
d lylxulpl b46a9d31
c kmkuslsw cebde86a
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Should error if a loop will be created.
let stderr = test_env.jj_cmd_failure(
&repo_path,
&["rebase", "-r", "e", "--after", "a", "--after", "b2"],
);
insta::assert_snapshot!(stderr, @r###"
Error: Refusing to create a loop: commit 2b8e1148290f would be both an ancestor and a descendant of the rebased commits
"###);
}
#[test]
fn test_rebase_revisions_before() {
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, "b1", &["a"]);
create_commit(&test_env, &repo_path, "b2", &["b1"]);
create_commit(&test_env, &repo_path, "b3", &["a"]);
create_commit(&test_env, &repo_path, "b4", &["b3"]);
create_commit(&test_env, &repo_path, "c", &["b2", "b4"]);
create_commit(&test_env, &repo_path, "d", &["c"]);
create_commit(&test_env, &repo_path, "e", &["c"]);
create_commit(&test_env, &repo_path, "f", &["e"]);
// Test the setup
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e4a00798
e nkmrtpmo 858693f7
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
let setup_opid = test_env.current_operation_id(&repo_path);
// Rebasing a commit before its children should be a no-op.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "c", "--before", "d", "--before", "e"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Skipping rebase of commits:
kmkuslsw cd86b3e4 c | c
lylxulpl 7d0512e5 d | d
nkmrtpmo 858693f7 e | e
xznxytkn e4a00798 f | f
Nothing changed.
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e4a00798
e nkmrtpmo 858693f7
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
// Rebasing a commit before itself should be a no-op.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "--before", "c"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Skipping rebase of commits:
kmkuslsw cd86b3e4 c | c
lylxulpl 7d0512e5 d | d
nkmrtpmo 858693f7 e | e
xznxytkn e4a00798 f | f
Nothing changed.
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn e4a00798
e nkmrtpmo 858693f7
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
// Rebasing a commit before the root commit should error.
let stderr = test_env.jj_cmd_failure(&repo_path, &["rebase", "-r", "c", "--before", "root()"]);
insta::assert_snapshot!(stderr, @r###"
Error: The root commit 000000000000 is immutable
"###);
// Rebase a commit before another commit. "c" has parents "b2" and "b4", so its
// children "d" and "e" should be rebased onto "b2" and "b4" respectively.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "c", "--before", "a"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 8 descendant commits
Working copy now at: xznxytkn 24335685 f | f
Parent commit : nkmrtpmo e9a28d4b e | e
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 24335685
e nkmrtpmo e9a28d4b
d lylxulpl 6609e9c6
b4 znkkpsqq 4b39b18c
b3 vruxwmqv 39f79dcc
b2 royxmykx ffcf6038
b1 zsuskuln 85e90af6
a rlvkpnrz 318ea816
c kmkuslsw 5f99791e
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit before its parent.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--before", "e"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 1 descendant commits
Working copy now at: xznxytkn 8e3b728a f | f
Parent commit : kmkuslsw cd86b3e4 c | c
Added 0 files, modified 0 files, removed 1 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 41706bd9
@ f xznxytkn 8e3b728a
d lylxulpl 7d0512e5
c kmkuslsw cd86b3e4
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit before a commit in a branch of a merge commit.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--before", "b2"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 4 descendant commits
Working copy now at: xznxytkn 2b4f48f8 f | f
Parent commit : zsuskuln 072d5ae1 b1 | b1
Added 0 files, modified 0 files, removed 5 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 7cad61fd
d lylxulpl 526b6ab6
c kmkuslsw 445f6927
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 972bfeb7
@ f xznxytkn 2b4f48f8
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit before the first commit in a branch of a merge commit.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--before", "b1"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 5 descendant commits
Working copy now at: xznxytkn 488ebb95 f | f
Parent commit : rlvkpnrz 2443ea76 a | a
Added 0 files, modified 0 files, removed 6 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 9d5fa6a2
d lylxulpl ca323694
c kmkuslsw 07426e1a
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 55376058
b1 zsuskuln cd5b1d04
@ f xznxytkn 488ebb95
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit before a merge commit. "c" has two parents "b2" and "b4", so
// the rebased commit "f" will have the two commits "b2" and "b4" as its
// parents.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "f", "--before", "c"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 3 descendant commits
Working copy now at: xznxytkn aae1bc10 f | f
Parent commit : royxmykx 2b8e1148 b2 | b2
Parent commit : znkkpsqq a52a83a4 b4 | b4
Added 0 files, modified 0 files, removed 2 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 0ea67093
d lylxulpl c079568d
c kmkuslsw 6371742b
@ f xznxytkn aae1bc10
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx 2b8e1148
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit before multiple commits.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "b1", "--before", "d", "--before", "e"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 5 descendant commits
Working copy now at: xznxytkn 8268ec4d f | f
Parent commit : nkmrtpmo fd26fbd4 e | e
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 8268ec4d
e nkmrtpmo fd26fbd4
d lylxulpl 21da64b4
b1 zsuskuln 83e9b8ac
c kmkuslsw a89354fc
b4 znkkpsqq a52a83a4
b3 vruxwmqv 523e6a8b
b2 royxmykx b7f03180
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a commit before two commits in separate branches to create a merge
// commit.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "f", "--before", "b2", "--before", "b4"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 1 commits onto destination
Rebased 5 descendant commits
Working copy now at: xznxytkn 7ba8014f f | f
Parent commit : zsuskuln 072d5ae1 b1 | b1
Parent commit : vruxwmqv 523e6a8b b3 | b3
Added 0 files, modified 0 files, removed 4 files
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
e nkmrtpmo 9436134a
d lylxulpl 534be1ee
c kmkuslsw bc3ed9f8
b4 znkkpsqq 3e59611b
b2 royxmykx 148d7e50
@ f xznxytkn 7ba8014f
b3 vruxwmqv 523e6a8b
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase two unrelated commits "b2" and "b4" before a single commit "a". This
// creates a merge commit "a" with the two parents "b2" and "b4".
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "b2", "-r", "b4", "--before", "a"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 2 commits onto destination
Rebased 7 descendant commits
Working copy now at: xznxytkn fabd8dd7 f | f
Parent commit : nkmrtpmo b5933877 e | e
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn fabd8dd7
e nkmrtpmo b5933877
d lylxulpl 6b91dd66
c kmkuslsw d873acf7
b3 vruxwmqv 1fd332d8
b1 zsuskuln 8e39430f
a rlvkpnrz 414580f5
b4 znkkpsqq ae3d5bdb
b2 royxmykx a225236e
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a subgraph with a merge commit and two parents.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "b2", "-r", "b4", "-r", "c", "--before", "e"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 3 commits onto destination
Rebased 3 descendant commits
Working copy now at: xznxytkn cbe2be58 f | f
Parent commit : nkmrtpmo e31053d1 e | e
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn cbe2be58
e nkmrtpmo e31053d1
c kmkuslsw 23155860
b4 znkkpsqq e50520ad
b2 royxmykx 54f03b06
d lylxulpl 0c74206e
b3 vruxwmqv 523e6a8b
b1 zsuskuln 072d5ae1
a rlvkpnrz 2443ea76
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Rebase a subgraph with disconnected commits. Since "b1" is an ancestor of
// "e", "b1" should be a parent of "e" after the rebase.
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&["rebase", "-r", "b1", "-r", "e", "--before", "a"],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Rebased 2 commits onto destination
Rebased 7 descendant commits
Working copy now at: xznxytkn 1c48b514 f | f
Parent commit : kmkuslsw c0fd979a c | c
"###);
insta::assert_snapshot!(get_long_log_output(&test_env, &repo_path), @r###"
@ f xznxytkn 1c48b514
d lylxulpl 4dbbc808
c kmkuslsw c0fd979a
b4 znkkpsqq 4d5c61f4
b3 vruxwmqv d5699c24
b2 royxmykx e23ab998
a rlvkpnrz 076f0094
e nkmrtpmo 20d1f131
b1 zsuskuln 11db739a
zzzzzzzz 00000000
"###);
test_env.jj_cmd_ok(&repo_path, &["op", "restore", &setup_opid]);
// Should error if a loop will be created.
let stderr = test_env.jj_cmd_failure(
&repo_path,
&["rebase", "-r", "e", "--before", "b2", "--before", "c"],
);
insta::assert_snapshot!(stderr, @r###"
Error: Refusing to create a loop: commit 2b8e1148290f would be both an ancestor and a descendant of the rebased commits
"###);
}
#[test] #[test]
fn test_rebase_skip_empty() { fn test_rebase_skip_empty() {
let test_env = TestEnvironment::default(); let test_env = TestEnvironment::default();

View file

@ -258,8 +258,7 @@ parent.
</tr> </tr>
<tr> <tr>
<td>Reorder changes from A-B-C-D to A-C-B-D</td> <td>Reorder changes from A-B-C-D to A-C-B-D</td>
<td><code>jj rebase -r C -d A; jj rebase -s B -d C</code> (pass change IDs, <td><code>jj rebase -r C --before B</code></td>
not commit IDs, to not have to look up commit ID of rewritten C)</td>
<td><code>git rebase -i A</code></td> <td><code>git rebase -i A</code></td>
</tr> </tr>
<tr> <tr>