ok/jj
1
0
Fork 0
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:
Ilya Grigoriev 2023-01-09 22:54:10 -08:00
parent 12ee2b18cd
commit f2114f63ee
5 changed files with 240 additions and 37 deletions

View file

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

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

View file

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

View file

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

View file

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