rewrite: add conflict labels for squashed commits

An example of squashing `ysrnknol` into `rtsqusxu`:

```
<<<<<<< conflict 1 of 1
+++++++ rtsqusxu 2768b0b9 "A" (squash destination)
updated in destination
%%%%%%% diff from: vpxusssl 38d49363 "B" (parents of squashed commit)
\\\\\\\        to: ysrnknol 7a20f389 "C" (squashed commit)
-base
+squashed
>>>>>>> conflict 1 of 1 ends
```

Or with only partial changes selected:

```
<<<<<<< conflict 1 of 1
+++++++ rtsqusxu 2768b0b9 "A" (squash destination)
updated in destination
%%%%%%% diff from: vpxusssl 38d49363 "B" (parents of squashed commit)
\\\\\\\        to: selected changes for squash (from ysrnknol 7a20f389 "C")
-base
+selected changes
>>>>>>> conflict 1 of 1 ends
```
This commit is contained in:
Scott Taylor 2025-10-18 09:45:48 -05:00
parent 225bbe4835
commit 5fd1b2fd5b
2 changed files with 45 additions and 36 deletions

View file

@ -903,10 +903,10 @@ fn test_squash_from_multiple() {
insta::assert_snapshot!(output, @r"
------- stderr -------
Rebased 2 descendant commits
Working copy (@) now at: kpqxywon 0b695306 f | (no description set)
Parent commit (@-) : yostqsxw ff064d52 e | (no description set)
Working copy (@) now at: kpqxywon eb200347 f | (no description set)
Parent commit (@-) : yostqsxw 9475acea e | (no description set)
New conflicts appeared in 1 commits:
yqosqzyt 61130da4 d | (conflict) (no description set)
yqosqzyt 33563014 d | (conflict) (no description set)
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
jj new yqosqzyt
@ -916,10 +916,10 @@ fn test_squash_from_multiple() {
[EOF]
");
insta::assert_snapshot!(get_log_output(&work_dir), @r"
@ 0b6953066ee0 f
ff064d529578 e
@ eb200347027e f
9475acea2503 e
× 61130da4e714 d
× 335630141fde d
e88768e65e67 a b c
000000000000 (empty)
@ -929,13 +929,15 @@ fn test_squash_from_multiple() {
let output = work_dir.run_jj(["file", "show", "-r=d", "file"]);
insta::assert_snapshot!(output, @r"
<<<<<<< conflict 1 of 1
%%%%%%% diff from base #1 to side #1
%%%%%%% diff from: qpvuntsm e88768e6 (parents of squashed commit)
\\\\\\\ to: yqosqzyt 8acbb715 (squash destination)
-a
+d
%%%%%%% diff from base #2 to side #2
%%%%%%% diff from: qpvuntsm e88768e6 (parents of squashed commit)
\\\\\\\ to: kkmpptxz fed4d1a2 (squashed commit)
-a
+b
+++++++ side #3
+++++++ mzvwutvl d7e94ec7 (squashed commit)
c
>>>>>>> conflict 1 of 1 ends
[EOF]
@ -1047,10 +1049,10 @@ fn test_squash_from_multiple_partial() {
insta::assert_snapshot!(output, @r"
------- stderr -------
Rebased 2 descendant commits
Working copy (@) now at: kpqxywon a724910c f | (no description set)
Parent commit (@-) : yostqsxw 1bc405e1 e | (no description set)
Working copy (@) now at: kpqxywon a8eee959 f | (no description set)
Parent commit (@-) : yostqsxw fac4927e e | (no description set)
New conflicts appeared in 1 commits:
yqosqzyt 7ddfe685 d | (conflict) (no description set)
yqosqzyt 22dccf0d d | (conflict) (no description set)
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
jj new yqosqzyt
@ -1060,13 +1062,13 @@ fn test_squash_from_multiple_partial() {
[EOF]
");
insta::assert_snapshot!(get_log_output(&work_dir), @r"
@ a724910cd361 f
1bc405e12b68 e
@ a8eee9597ba5 f
fac4927e5714 e
e9db15b956c4 b
83cbe51db94d c
× 7ddfe6857387 d
× 22dccf0db9ab d
64ea60be8d77 a
000000000000 (empty)
@ -1087,13 +1089,15 @@ fn test_squash_from_multiple_partial() {
let output = work_dir.run_jj(["file", "show", "-r=d", "file1"]);
insta::assert_snapshot!(output, @r"
<<<<<<< conflict 1 of 1
%%%%%%% diff from base #1 to side #1
%%%%%%% diff from: qpvuntsm 64ea60be (parents of squashed commit)
\\\\\\\ to: yqosqzyt f6812ff8 (squash destination)
-a
+d
%%%%%%% diff from base #2 to side #2
%%%%%%% diff from: qpvuntsm 64ea60be (parents of squashed commit)
\\\\\\\ to: selected changes for squash (from kkmpptxz f2c9709f)
-a
+b
+++++++ side #3
+++++++ selected changes for squash (from mzvwutvl aa908686)
c
>>>>>>> conflict 1 of 1 ends
[EOF]

View file

@ -1174,6 +1174,7 @@ impl CommitWithSelection {
&self,
parent_tree_label: &str,
selected_tree_label: &str,
full_selection_label: &str,
) -> BackendResult<Diff<(MergedTree, String)>> {
let parents: Vec<_> = self.commit.parents().try_collect()?;
let parent_tree_label = format!(
@ -1183,9 +1184,9 @@ impl CommitWithSelection {
let commit_label = self.commit.conflict_label();
let selected_tree_label = if self.is_full_selection() {
format!("{commit_label} ({selected_tree_label})")
format!("{commit_label} ({full_selection_label})")
} else {
format!("{selected_tree_label} (selected from {commit_label})")
format!("{selected_tree_label} (from {commit_label})")
};
Ok(Diff::new(
@ -1215,6 +1216,7 @@ pub fn squash_commits<'repo>(
) -> BackendResult<Option<SquashedCommit<'repo>>> {
struct SourceCommit<'a> {
commit: &'a CommitWithSelection,
diff: Diff<(MergedTree, String)>,
abandon: bool,
}
let mut source_commits = vec![];
@ -1231,6 +1233,11 @@ pub fn squash_commits<'repo>(
// squash -r`)? The source tree will be unchanged in that case.
source_commits.push(SourceCommit {
commit: source,
diff: source.diff_with_labels(
"parents of squashed commit",
"selected changes for squash",
"squashed commit",
)?,
abandon,
});
}
@ -1247,12 +1254,11 @@ pub fn squash_commits<'repo>(
} else {
let source_tree = source.commit.commit.tree();
// Apply the reverse of the selected changes onto the source
let new_source_tree = source_tree
.merge_unlabeled(
source.commit.selected_tree.clone(),
source.commit.parent_tree.clone(),
)
.block_on()?;
let new_source_tree = MergedTree::merge(Merge::from_diffs(
(source_tree, source.commit.commit.conflict_label()),
[source.diff.clone().invert()],
))
.block_on()?;
repo.rewrite_commit(&source.commit.commit)
.set_tree(new_source_tree)
.write()?;
@ -1282,22 +1288,21 @@ pub fn squash_commits<'repo>(
};
})?;
}
// Apply the selected changes onto the destination
let mut destination_tree = rewritten_destination.tree();
for source in &source_commits {
destination_tree = destination_tree
.merge_unlabeled(
source.commit.parent_tree.clone(),
source.commit.selected_tree.clone(),
)
.block_on()?;
}
let mut predecessors = vec![destination.id().clone()];
predecessors.extend(
source_commits
.iter()
.map(|source| source.commit.commit.id().clone()),
);
// Apply the selected changes onto the destination
let destination_tree = MergedTree::merge(Merge::from_diffs(
(
rewritten_destination.tree(),
format!("{} (squash destination)", destination.conflict_label()),
),
source_commits.into_iter().map(|source| source.diff),
))
.block_on()?;
let commit_builder = repo
.rewrite_commit(&rewritten_destination)