Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
aliases that are simply repeated. For example, with the alias `jj = []`, the
command `jj jj jj log` will resolve to `jj log` as expected.

* `jj diff` and `jj status` no longer omit a rename or copy in a merge commit
when the renamed source is present in more than one parent. The identical copy
record reported for each parent was mistaken for a conflict and discarded.
[#9752](https://github.com/jj-vcs/jj/issues/9752)

## [0.43.0] - 2026-07-01

### Release highlights
Expand Down
29 changes: 29 additions & 0 deletions cli/tests/test_diff_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4089,3 +4089,32 @@ fn test_diff_revisions() {
[EOF]
");
}

#[test]
fn test_diff_rename_in_merge_commit() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");

// A rename in a merge commit is detected once per parent. When the renamed
// source is present in more than one parent, the same copy record is reported
// for each, and those duplicates must not cancel each other out and drop the
// rename. https://github.com/jj-vcs/jj/issues/9752
work_dir.write_file("a.txt", "aaa\n");
work_dir.run_jj(["describe", "-m", "base"]).success();
work_dir.run_jj(["new", "-m", "first branch"]).success();
work_dir
.run_jj(["new", "@-", "-m", "second branch"])
.success();
// Merge both children of "base" (@-+); each holds a.txt, so the rename is
// detected against both parents.
work_dir.run_jj(["new", "@-+", "-m", "merge"]).success();
work_dir.remove_file("a.txt");
work_dir.write_file("b.txt", "aaa\n");

let output = work_dir.run_jj(["diff", "-s"]);
insta::assert_snapshot!(output, @"
R {a.txt => b.txt}
[EOF]
");
}
2 changes: 1 addition & 1 deletion cli/tests/test_diffedit_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ fn test_diffedit_merge() -> TestResult {
let output = work_dir.run_jj(["diff", "-r", "@-", "-s"]);
insta::assert_snapshot!(output, @"
M file1
A file3
C {file1 => file3}
[EOF]
");

Expand Down
12 changes: 12 additions & 0 deletions lib/src/copies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ impl CopyRecords {
/// conflicts is discarded and treated as not having an origin.
pub fn add_records(&mut self, copy_records: impl IntoIterator<Item = CopyRecord>) {
for r in copy_records {
// The same copy or rename is reported once per parent when diffing a
// merge commit. Identical (source, target) pairs describe the same
// operation, so skip the duplicate instead of marking both maps as
// conflicting, which would otherwise drop the copy/rename entirely.
let is_duplicate = self
.targets
.get(&r.target)
.and_then(|&i| self.records.get(i))
.is_some_and(|existing| existing.source == r.source);
if is_duplicate {
continue;
}
self.sources
.entry(r.source.clone())
// TODO: handle conflicts instead of ignoring both sides.
Expand Down