forked from mirrors/jj
jj duplicate
: Allow duplicating several commits at once
The `indexmap` crate is used to make `duplicate`'s output have a sane order, making it easier to test. It's also used later to remove duplicate revisions in the `abandon` command.
This commit is contained in:
parent
12ee2b18cd
commit
f2114f63ee
5 changed files with 240 additions and 37 deletions
|
@ -63,6 +63,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
operations ("origin" by default) using the `git.fetch` and `git.push`
|
||||
configuration entries.
|
||||
|
||||
* `jj duplicate` can now duplicate multiple changes in one go. This preserves
|
||||
any parent-child relationships between them. For example, the entire tree of
|
||||
descendants of `abc` can be duplicated with `jj duplicate abc:`.
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* When sharing the working copy with a Git repo, we used to forget to export
|
||||
|
|
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -877,9 +877,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.1"
|
||||
version = "1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
|
||||
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
|
@ -989,6 +989,7 @@ dependencies = [
|
|||
"git2",
|
||||
"glob",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools",
|
||||
"jujutsu-lib",
|
||||
|
|
|
@ -61,6 +61,7 @@ timeago = { version = "0.4.0", default-features = false }
|
|||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.16", default-features = false, features = ["std", "ansi", "env-filter", "fmt"] }
|
||||
indexmap = "1.9.2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { version = "0.2.139" }
|
||||
|
|
100
src/commands.rs
100
src/commands.rs
|
@ -25,6 +25,7 @@ use std::{fs, io};
|
|||
use clap::builder::NonEmptyStringValueParser;
|
||||
use clap::{ArgGroup, ArgMatches, CommandFactory, FromArgMatches, Subcommand};
|
||||
use config::Source;
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use itertools::Itertools;
|
||||
use jujutsu_lib::backend::{CommitId, ObjectId, TreeValue};
|
||||
use jujutsu_lib::commit::Commit;
|
||||
|
@ -397,9 +398,9 @@ struct CommitArgs {
|
|||
/// Create a new change with the same content as an existing one
|
||||
#[derive(clap::Args, Clone, Debug)]
|
||||
struct DuplicateArgs {
|
||||
/// The revision to duplicate
|
||||
/// The revision(s) to duplicate
|
||||
#[arg(default_value = "@")]
|
||||
revision: RevisionArg,
|
||||
revisions: Vec<RevisionArg>,
|
||||
/// Ignored (but lets you pass `-r` for consistency with other commands)
|
||||
#[arg(short = 'r', hide = true)]
|
||||
unused_revision: bool,
|
||||
|
@ -2034,30 +2035,80 @@ fn cmd_commit(ui: &mut Ui, command: &CommandHelper, args: &CommitArgs) -> Result
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_multiple_rewriteable_revsets(
|
||||
revision_args: &[RevisionArg],
|
||||
workspace_command: &WorkspaceCommandHelper,
|
||||
) -> Result<Vec<Commit>, CommandError> {
|
||||
let mut acc = Vec::new();
|
||||
for revset in revision_args {
|
||||
let revisions = workspace_command.resolve_revset(revset)?;
|
||||
workspace_command.check_non_empty(&revisions)?;
|
||||
for commit in &revisions {
|
||||
workspace_command.check_rewriteable(commit)?;
|
||||
}
|
||||
acc.extend(revisions);
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
fn cmd_duplicate(
|
||||
ui: &mut Ui,
|
||||
command: &CommandHelper,
|
||||
args: &DuplicateArgs,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let predecessor = workspace_command.resolve_single_rev(&args.revision)?;
|
||||
workspace_command.check_rewriteable(&predecessor)?;
|
||||
let to_duplicate: IndexSet<Commit> =
|
||||
resolve_multiple_rewriteable_revsets(&args.revisions, &workspace_command)?
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut duplicated_old_to_new: IndexMap<Commit, Commit> = IndexMap::new();
|
||||
|
||||
let mut tx = workspace_command
|
||||
.start_transaction(&format!("duplicate commit {}", predecessor.id().hex()));
|
||||
.start_transaction(&format!("duplicating {} commit(s)", to_duplicate.len()));
|
||||
let index = tx.base_repo().index().clone();
|
||||
let store = tx.base_repo().store().clone();
|
||||
let mut_repo = tx.mut_repo();
|
||||
let new_commit = mut_repo
|
||||
.rewrite_commit(command.settings(), &predecessor)
|
||||
.generate_new_change_id()
|
||||
.write()?;
|
||||
ui.write("Created: ")?;
|
||||
write_commit_summary(
|
||||
ui.stdout_formatter().as_mut(),
|
||||
mut_repo.as_repo_ref(),
|
||||
&workspace_command.workspace_id(),
|
||||
&new_commit,
|
||||
command.settings(),
|
||||
)?;
|
||||
ui.write("\n")?;
|
||||
|
||||
for original_commit_id in index
|
||||
.topo_order(to_duplicate.iter().map(|c| c.id()))
|
||||
.into_iter()
|
||||
.map(|index_entry| index_entry.commit_id())
|
||||
{
|
||||
// Topological order ensures that any parents of `original_commit` are
|
||||
// either not in `to_duplicate` or were already duplicated.
|
||||
let original_commit = store.get_commit(&original_commit_id).unwrap();
|
||||
let new_parents = original_commit
|
||||
.parents()
|
||||
.iter()
|
||||
.map(|parent| {
|
||||
if let Some(duplicated_parent) = duplicated_old_to_new.get(parent) {
|
||||
duplicated_parent
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
.id()
|
||||
.clone()
|
||||
})
|
||||
.collect();
|
||||
let new_commit = mut_repo
|
||||
.rewrite_commit(command.settings(), &original_commit)
|
||||
.generate_new_change_id()
|
||||
.set_parents(new_parents)
|
||||
.write()?;
|
||||
duplicated_old_to_new.insert(original_commit, new_commit);
|
||||
}
|
||||
|
||||
for (old, new) in duplicated_old_to_new.iter() {
|
||||
ui.write(&format!("Duplicated {} as ", short_commit_hash(old.id())))?;
|
||||
write_commit_summary(
|
||||
ui.stdout_formatter().as_mut(),
|
||||
mut_repo.as_repo_ref(),
|
||||
&workspace_command.workspace_id(),
|
||||
new,
|
||||
command.settings(),
|
||||
)?;
|
||||
ui.write("\n")?;
|
||||
}
|
||||
workspace_command.finish_transaction(ui, tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -2068,18 +2119,7 @@ fn cmd_abandon(
|
|||
args: &AbandonArgs,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let to_abandon = {
|
||||
let mut acc = Vec::new();
|
||||
for revset in &args.revisions {
|
||||
let revisions = workspace_command.resolve_revset(revset)?;
|
||||
workspace_command.check_non_empty(&revisions)?;
|
||||
for commit in &revisions {
|
||||
workspace_command.check_rewriteable(commit)?;
|
||||
}
|
||||
acc.extend(revisions);
|
||||
}
|
||||
acc
|
||||
};
|
||||
let to_abandon = resolve_multiple_rewriteable_revsets(&args.revisions, &workspace_command)?;
|
||||
let transaction_description = if to_abandon.len() == 1 {
|
||||
format!("abandon commit {}", to_abandon[0].id().hex())
|
||||
} else {
|
||||
|
|
|
@ -56,7 +56,7 @@ fn test_duplicate() {
|
|||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "a"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Created: 2f6dc5a1ffc2 a
|
||||
Duplicated 2443ea76b0b1 as 2f6dc5a1ffc2 a
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o 2f6dc5a1ffc2 a
|
||||
|
@ -72,7 +72,7 @@ fn test_duplicate() {
|
|||
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["undo"]), @"");
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate" /* duplicates `c` */]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Created: 1dd099ea963c c
|
||||
Duplicated 17a00fc21654 as 1dd099ea963c c
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o 1dd099ea963c c
|
||||
|
@ -89,6 +89,163 @@ fn test_duplicate() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_many() {
|
||||
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", &["a"]);
|
||||
create_commit(&test_env, &repo_path, "d", &["c"]);
|
||||
create_commit(&test_env, &repo_path, "e", &["b", "d"]);
|
||||
// Test the setup
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
@ 921dde6e55c0 e
|
||||
|\
|
||||
o | ebd06dba20ec d
|
||||
o | c0cb3a0b73e7 c
|
||||
| o 1394f625cbbd b
|
||||
|/
|
||||
o 2443ea76b0b1 a
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "b:"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Duplicated 1394f625cbbd as 3b74d9691015 b
|
||||
Duplicated 921dde6e55c0 as 8348ddcec733 e
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o 8348ddcec733 e
|
||||
|\
|
||||
o | 3b74d9691015 b
|
||||
| | @ 921dde6e55c0 e
|
||||
| | |\
|
||||
| |/ /
|
||||
| o | ebd06dba20ec d
|
||||
| o | c0cb3a0b73e7 c
|
||||
|/ /
|
||||
| o 1394f625cbbd b
|
||||
|/
|
||||
o 2443ea76b0b1 a
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
|
||||
// Try specifying the same commit twice directly
|
||||
test_env.jj_cmd_success(&repo_path, &["undo"]);
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "b", "b"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Duplicated 1394f625cbbd as 0276d3d7c24d b
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o 0276d3d7c24d b
|
||||
| @ 921dde6e55c0 e
|
||||
| |\
|
||||
| o | ebd06dba20ec d
|
||||
| o | c0cb3a0b73e7 c
|
||||
|/ /
|
||||
| o 1394f625cbbd b
|
||||
|/
|
||||
o 2443ea76b0b1 a
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
|
||||
// Try specifying the same commit twice indirectly
|
||||
test_env.jj_cmd_success(&repo_path, &["undo"]);
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "b:", "d:"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Duplicated 1394f625cbbd as fa167d18a83a b
|
||||
Duplicated ebd06dba20ec as 2181781b4f81 d
|
||||
Duplicated 921dde6e55c0 as 0f7430f2727a e
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o 0f7430f2727a e
|
||||
|\
|
||||
o | 2181781b4f81 d
|
||||
| o fa167d18a83a b
|
||||
| | @ 921dde6e55c0 e
|
||||
| | |\
|
||||
| | o | ebd06dba20ec d
|
||||
| |/ /
|
||||
|/| |
|
||||
o | | c0cb3a0b73e7 c
|
||||
|/ /
|
||||
| o 1394f625cbbd b
|
||||
|/
|
||||
o 2443ea76b0b1 a
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
|
||||
test_env.jj_cmd_success(&repo_path, &["undo"]);
|
||||
// Reminder of the setup
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
@ 921dde6e55c0 e
|
||||
|\
|
||||
o | ebd06dba20ec d
|
||||
o | c0cb3a0b73e7 c
|
||||
| o 1394f625cbbd b
|
||||
|/
|
||||
o 2443ea76b0b1 a
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "d:", "a"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Duplicated 2443ea76b0b1 as c6f7f8c4512e a
|
||||
Duplicated ebd06dba20ec as d94e4c55a68b d
|
||||
Duplicated 921dde6e55c0 as 9bd4389f5d47 e
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o 9bd4389f5d47 e
|
||||
|\
|
||||
o | d94e4c55a68b d
|
||||
| | o c6f7f8c4512e a
|
||||
| | | @ 921dde6e55c0 e
|
||||
| | | |\
|
||||
| | |_|/
|
||||
| |/| |
|
||||
| | | o ebd06dba20ec d
|
||||
| |_|/
|
||||
|/| |
|
||||
o | | c0cb3a0b73e7 c
|
||||
| o | 1394f625cbbd b
|
||||
|/ /
|
||||
o | 2443ea76b0b1 a
|
||||
|/
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
|
||||
// Check for BUG -- makes too many 'a'-s, etc.
|
||||
test_env.jj_cmd_success(&repo_path, &["undo"]);
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "a:"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Duplicated 2443ea76b0b1 as 0fe67a05989e a
|
||||
Duplicated 1394f625cbbd as e13ac0adabdf b
|
||||
Duplicated c0cb3a0b73e7 as df53fa589286 c
|
||||
Duplicated ebd06dba20ec as 2f2442db08eb d
|
||||
Duplicated 921dde6e55c0 as ee8fe64ed254 e
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o ee8fe64ed254 e
|
||||
|\
|
||||
o | 2f2442db08eb d
|
||||
o | df53fa589286 c
|
||||
| o e13ac0adabdf b
|
||||
|/
|
||||
o 0fe67a05989e a
|
||||
| @ 921dde6e55c0 e
|
||||
| |\
|
||||
| o | ebd06dba20ec d
|
||||
| o | c0cb3a0b73e7 c
|
||||
| | o 1394f625cbbd b
|
||||
| |/
|
||||
| o 2443ea76b0b1 a
|
||||
|/
|
||||
o 000000000000 (no description set)
|
||||
"###);
|
||||
}
|
||||
|
||||
// https://github.com/martinvonz/jj/issues/1050
|
||||
#[test]
|
||||
fn test_undo_after_duplicate() {
|
||||
|
@ -104,7 +261,7 @@ fn test_undo_after_duplicate() {
|
|||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "a"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Created: f5cefcbb65a4 a
|
||||
Duplicated 2443ea76b0b1 as f5cefcbb65a4 a
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
|
||||
o f5cefcbb65a4 a
|
||||
|
@ -138,11 +295,11 @@ fn test_rebase_duplicates() {
|
|||
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "b"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Created: fdaaf3950f07 b
|
||||
Duplicated 1394f625cbbd as fdaaf3950f07 b
|
||||
"###);
|
||||
let stdout = test_env.jj_cmd_success(&repo_path, &["duplicate", "b"]);
|
||||
insta::assert_snapshot!(stdout, @r###"
|
||||
Created: 870cf438ccbb b
|
||||
Duplicated 1394f625cbbd as 870cf438ccbb b
|
||||
"###);
|
||||
insta::assert_snapshot!(get_log_output_with_ts(&test_env, &repo_path), @r###"
|
||||
o 870cf438ccbb b @ 2001-02-03 04:05:14.000 +07:00
|
||||
|
|
Loading…
Reference in a new issue