cli: diff: support multiple revisions to -r

This commit is contained in:
Martin von Zweigbergk 2025-03-08 13:39:18 -08:00
parent f862d89143
commit dc7216d73a
6 changed files with 155 additions and 17 deletions

View File

@ -38,6 +38,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New features
* `jj diff -r` now allows multiple revisions (as long as there are no gaps in
the revset), such as `jj diff -r 'mutable()'`.
* The 'how to resolve conflicts' hint that is shown when conflicts appear can
be hidden by setting `hints.resolving-conflicts = false`.

View File

@ -14,6 +14,7 @@
use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use indexmap::IndexSet;
use itertools::Itertools;
use jj_lib::copies::CopyRecords;
use jj_lib::repo::Repo;
@ -21,8 +22,10 @@ use jj_lib::rewrite::merge_commit_trees;
use tracing::instrument;
use crate::cli_util::print_unmatched_explicit_paths;
use crate::cli_util::short_commit_hash;
use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::command_error::user_error_with_hint;
use crate::command_error::CommandError;
use crate::complete;
use crate::diff_util::get_copy_records;
@ -44,23 +47,30 @@ use crate::ui::Ui;
#[command(mut_arg("ignore_all_space", |a| a.short('w')))]
#[command(mut_arg("ignore_space_change", |a| a.short('b')))]
pub(crate) struct DiffArgs {
/// Show changes in this revision, compared to its parent(s)
/// Show changes in these revisions
///
/// If the revision is a merge commit, this shows changes *from* the
/// If there are multiple revisions, then then total diff for all of them
/// will be shown. For example, if you have a linear chain of revisions
/// A..D, then `jj diff -r B::D` equals `jj diff --from A --to D`. Multiple
/// heads and/or roots are supported, but gaps in the revset are not
/// supported (e.g. `jj diff -r 'A|C'` in a linear chain A..C).
///
/// If a revision is a merge commit, this shows changes *from* the
/// automatic merge of the contents of all of its parents *to* the contents
/// of the revision itself.
#[arg(
long,
short,
value_name = "REVSET",
value_name = "REVSETS",
alias = "revision",
add = ArgValueCandidates::new(complete::all_revisions)
)]
revision: Option<RevisionArg>,
revisions: Option<Vec<RevisionArg>>,
/// Show changes from this revision
#[arg(
long,
short,
conflicts_with = "revision",
conflicts_with = "revisions",
value_name = "REVSET",
add = ArgValueCandidates::new(complete::all_revisions)
)]
@ -69,7 +79,7 @@ pub(crate) struct DiffArgs {
#[arg(
long,
short,
conflicts_with = "revision",
conflicts_with = "revisions",
value_name = "REVSET",
add = ArgValueCandidates::new(complete::all_revisions)
)]
@ -95,14 +105,14 @@ pub(crate) fn cmd_diff(
let repo = workspace_command.repo();
let fileset_expression = workspace_command.parse_file_patterns(ui, &args.paths)?;
let matcher = fileset_expression.to_matcher();
let resolve_revision = |r: &Option<RevisionArg>| {
workspace_command.resolve_single_rev(ui, r.as_ref().unwrap_or(&RevisionArg::AT))
};
let from_tree;
let to_tree;
let mut copy_records = CopyRecords::default();
if args.from.is_some() || args.to.is_some() {
let resolve_revision = |r: &Option<RevisionArg>| {
workspace_command.resolve_single_rev(ui, r.as_ref().unwrap_or(&RevisionArg::AT))
};
let from = resolve_revision(&args.from)?;
let to = resolve_revision(&args.to)?;
from_tree = from.tree()?;
@ -111,14 +121,44 @@ pub(crate) fn cmd_diff(
let records = get_copy_records(repo.store(), from.id(), to.id(), &matcher)?;
copy_records.add_records(records)?;
} else {
let to = resolve_revision(&args.revision)?;
let parents: Vec<_> = to.parents().try_collect()?;
let revision_args = args
.revisions
.as_deref()
.unwrap_or(std::slice::from_ref(&RevisionArg::AT));
let revisions_evaluator = workspace_command.parse_union_revsets(ui, revision_args)?;
let target_expression = revisions_evaluator.expression();
let mut gaps_revset = workspace_command
.attach_revset_evaluator(target_expression.connected().minus(target_expression))
.evaluate_to_commit_ids()?;
if let Some(commit_id) = gaps_revset.next() {
return Err(user_error_with_hint(
"Cannot diff revsets with gaps in.",
format!(
"Revision {} would need to be in the set.",
short_commit_hash(&commit_id?)
),
));
}
let heads: Vec<_> = workspace_command
.attach_revset_evaluator(target_expression.heads())
.evaluate_to_commits()?
.try_collect()?;
let roots: Vec<_> = workspace_command
.attach_revset_evaluator(target_expression.roots())
.evaluate_to_commits()?
.try_collect()?;
// Collect parents outside of revset to preserve parent order
let parents: IndexSet<_> = roots.iter().flat_map(|c| c.parents()).try_collect()?;
let parents = parents.into_iter().collect_vec();
from_tree = merge_commit_trees(repo.as_ref(), &parents)?;
to_tree = to.tree()?;
to_tree = merge_commit_trees(repo.as_ref(), &heads)?;
for p in &parents {
let records = get_copy_records(repo.store(), p.id(), to.id(), &matcher)?;
copy_records.add_records(records)?;
for to in &heads {
let records = get_copy_records(repo.store(), p.id(), to.id(), &matcher)?;
copy_records.add_records(records)?;
}
}
}

View File

@ -606,7 +606,7 @@ fn modified_files_from_rev_with_jj_cmd(
.arg("--summary")
.arg(current_prefix_to_fileset(current));
match rev {
(rev, None) => cmd.arg("--revision").arg(rev),
(rev, None) => cmd.arg("--revisions").arg(rev),
(from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to),
};
let output = cmd.output().map_err(user_error)?;

View File

@ -741,9 +741,11 @@ With the `--from` and/or `--to` options, shows the difference from/to the given
###### **Options:**
* `-r`, `--revision <REVSET>` — Show changes in this revision, compared to its parent(s)
* `-r`, `--revisions <REVSETS>` — Show changes in these revisions
If the revision is a merge commit, this shows changes *from* the automatic merge of the contents of all of its parents *to* the contents of the revision itself.
If there are multiple revisions, then then total diff for all of them will be shown. For example, if you have a linear chain of revisions A..D, then `jj diff -r B::D` equals `jj diff --from A --to D`. Multiple heads and/or roots are supported, but gaps in the revset are not supported (e.g. `jj diff -r 'A|C'` in a linear chain A..C).
If a revision is a merge commit, this shows changes *from* the automatic merge of the contents of all of its parents *to* the contents of the revision itself.
* `-f`, `--from <REVSET>` — Show changes from this revision
* `-t`, `--to <REVSET>` — Show changes to this revision
* `-s`, `--summary` — For each path, show only whether it was modified, added, or deleted

View File

@ -15,8 +15,10 @@
use indoc::indoc;
use itertools::Itertools;
use crate::common::create_commit;
use crate::common::fake_diff_editor_path;
use crate::common::to_toml_value;
use crate::common::CommandOutput;
use crate::common::TestEnvironment;
#[test]
@ -2843,3 +2845,90 @@ fn test_diff_binary() {
[EOF]
");
}
#[test]
fn test_diff_revisions() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
//
//
//
// E
// |\
// C D
// |/
// B
// |
// A
create_commit(&work_dir, "A", &[]);
create_commit(&work_dir, "B", &["A"]);
create_commit(&work_dir, "C", &["B"]);
create_commit(&work_dir, "D", &["B"]);
create_commit(&work_dir, "E", &["C", "D"]);
let diff_revisions = |expression: &str| -> CommandOutput {
work_dir.run_jj(["diff", "--name-only", "-r", expression])
};
// Can diff a single revision
insta::assert_snapshot!(diff_revisions("B"), @r"
B
[EOF]
");
// Can diff a merge
insta::assert_snapshot!(diff_revisions("E"), @r"
E
[EOF]
");
// A gap in the range is not allowed (yet at least)
insta::assert_snapshot!(diff_revisions("A|C"), @r"
------- stderr -------
Error: Cannot diff revsets with gaps in.
Hint: Revision 50c75fd767bf would need to be in the set.
[EOF]
[exit status: 1]
");
// Can diff a linear chain
insta::assert_snapshot!(diff_revisions("A::C"), @r"
A
B
C
[EOF]
");
// Can diff a chain with an internal merge
insta::assert_snapshot!(diff_revisions("B::E"), @r"
B
C
D
E
[EOF]
");
// Can diff a set with multiple roots
insta::assert_snapshot!(diff_revisions("C|D|E"), @r"
C
D
E
[EOF]
");
// Can diff a set with multiple heads
insta::assert_snapshot!(diff_revisions("B|C|D"), @r"
B
C
D
[EOF]
");
// Can diff a set with multiple root and multiple heads
insta::assert_snapshot!(diff_revisions("B|C"), @r"
B
C
[EOF]
");
}

View File

@ -79,6 +79,10 @@ parent.
<td><code>jj diff --from A --to B</code></td>
</tr>
<tr>
<td>Show all the changes in A..B</td>
<td><code>git diff A...B</code></td>
<td><code>jj diff -r A..B</code></td>
</tr>
<tr>
<td>Show description and diff of a change</td>
<td><code>git show &lt;revision&gt;</code></td>