diff --git a/CHANGELOG.md b/CHANGELOG.md index 568458d42b..dc22b6da6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). materialized in the working copy. The default option ("diff") renders conflicts as a snapshot with a list of diffs to apply to the snapshot. The new "snapshot" option just renders conflicts as a series of snapshots, - showing each side of the conflict and the base(s). + showing each side and base of the conflict. The new "git-diff3" option + replicates Git's "diff3" conflict style, meaning it is more likely to work + with external tools, but it doesn't support conflicts with more than 2 sides. ### Fixed bugs diff --git a/lib/src/conflicts.rs b/lib/src/conflicts.rs index 07f9bec724..9409cc0f68 100644 --- a/lib/src/conflicts.rs +++ b/lib/src/conflicts.rs @@ -55,11 +55,15 @@ const CONFLICT_END_LINE: &[u8] = b">>>>>>>"; const CONFLICT_DIFF_LINE: &[u8] = b"%%%%%%%"; const CONFLICT_MINUS_LINE: &[u8] = b"-------"; const CONFLICT_PLUS_LINE: &[u8] = b"+++++++"; +const CONFLICT_GIT_ANCESTOR_LINE: &[u8] = b"|||||||"; +const CONFLICT_GIT_SEPARATOR_LINE: &[u8] = b"======="; const CONFLICT_START_LINE_CHAR: u8 = CONFLICT_START_LINE[0]; const CONFLICT_END_LINE_CHAR: u8 = CONFLICT_END_LINE[0]; const CONFLICT_DIFF_LINE_CHAR: u8 = CONFLICT_DIFF_LINE[0]; const CONFLICT_MINUS_LINE_CHAR: u8 = CONFLICT_MINUS_LINE[0]; const CONFLICT_PLUS_LINE_CHAR: u8 = CONFLICT_PLUS_LINE[0]; +const CONFLICT_GIT_ANCESTOR_LINE_CHAR: u8 = CONFLICT_GIT_ANCESTOR_LINE[0]; +const CONFLICT_GIT_SEPARATOR_LINE_CHAR: u8 = CONFLICT_GIT_SEPARATOR_LINE[0]; /// A conflict marker is one of the separators, optionally followed by a space /// and some text. @@ -67,7 +71,7 @@ const CONFLICT_PLUS_LINE_CHAR: u8 = CONFLICT_PLUS_LINE[0]; // separators. This could be useful to make it possible to allow conflict // markers inside the text of the conflicts. static CONFLICT_MARKER_REGEX: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - RegexBuilder::new(r"^(<{7}|>{7}|%{7}|\-{7}|\+{7})( .*)?$") + RegexBuilder::new(r"^(<{7}|>{7}|%{7}|\-{7}|\+{7}|\|{7}|={7})( .*)?$") .multi_line(true) .build() .unwrap() @@ -143,6 +147,8 @@ pub enum ConflictMarkerStyle { Diff, /// Style which only shows a snapshot for each base and side Snapshot, + /// Style which replicates Git's "diff3" style to support external tools + GitDiff3, } /// A type similar to `MergedTreeValue` but with associated data to include in @@ -268,7 +274,13 @@ pub fn materialize_merge_result( format!(" Conflict {conflict_index} of {num_conflicts}\n").as_bytes(), )?; - materialize_jj_style_conflict(&hunk, conflict_marker_style, output)?; + if conflict_marker_style == ConflictMarkerStyle::GitDiff3 + && hunk.num_sides() == 2 + { + materialize_git_diff3_conflict(&hunk, output)?; + } else { + materialize_jj_style_conflict(&hunk, conflict_marker_style, output)?; + } output.write_all(CONFLICT_END_LINE)?; output.write_all( @@ -281,6 +293,26 @@ pub fn materialize_merge_result( Ok(()) } +fn materialize_git_diff3_conflict( + hunk: &Merge, + output: &mut dyn Write, +) -> std::io::Result<()> { + let [right1, left, right2] = hunk.as_slice() else { + unreachable!("can only materialize Git-style conflicts with 2 sides") + }; + + output.write_all(right1)?; + output.write_all(CONFLICT_GIT_ANCESTOR_LINE)?; + output.write_all(b" Contents of base\n")?; + output.write_all(left)?; + output.write_all(CONFLICT_GIT_SEPARATOR_LINE)?; + // VS Code doesn't seem to support any trailing text on the separator line + output.write_all(b"\n")?; + output.write_all(right2)?; + + Ok(()) +} + fn materialize_jj_style_conflict( hunk: &Merge, conflict_marker_style: ConflictMarkerStyle, @@ -452,31 +484,54 @@ pub fn parse_conflict(input: &[u8], num_sides: usize) -> Option Merge { enum State { Diff, Minus, Plus, - Unknown, + BeforeFirstMarker, } - let mut state = State::Unknown; + let mut state = State::BeforeFirstMarker; let mut removes = vec![]; let mut adds = vec![]; + let mut before_first_marker = BString::new(vec![]); + let mut git_style = false; for line in input.split_inclusive(|b| *b == b'\n') { if CONFLICT_MARKER_REGEX.is_match_at(line, 0) { match line[0] { - CONFLICT_DIFF_LINE_CHAR => { + CONFLICT_DIFF_LINE_CHAR if !git_style => { state = State::Diff; removes.push(BString::new(vec![])); adds.push(BString::new(vec![])); continue; } - CONFLICT_MINUS_LINE_CHAR => { + CONFLICT_MINUS_LINE_CHAR if !git_style => { + state = State::Minus; + removes.push(BString::new(vec![])); + continue; + } + CONFLICT_PLUS_LINE_CHAR if !git_style => { + state = State::Plus; + adds.push(BString::new(vec![])); + continue; + } + // There should be no other conflict markers before the Git ancestor marker + CONFLICT_GIT_ANCESTOR_LINE_CHAR if adds.is_empty() && removes.is_empty() => { state = State::Minus; removes.push(BString::new(vec![])); + adds.push(before_first_marker); + before_first_marker = BString::new(vec![]); + git_style = true; continue; } - CONFLICT_PLUS_LINE_CHAR => { + CONFLICT_GIT_SEPARATOR_LINE_CHAR if git_style => { state = State::Plus; adds.push(BString::new(vec![])); continue; @@ -504,14 +559,15 @@ fn parse_conflict_hunk(input: &[u8]) -> Merge { State::Plus => { adds.last_mut().unwrap().extend_from_slice(line); } - State::Unknown => { - // Doesn't look like a valid conflict - return Merge::resolved(BString::new(vec![])); + State::BeforeFirstMarker => { + // Only Git style has an "add" before the first marker + git_style = true; + before_first_marker.extend_from_slice(line); } } } - if adds.len() == removes.len() + 1 { + if before_first_marker.is_empty() && adds.len() == removes.len() + 1 { Merge::from_removes_adds(removes, adds) } else { // Doesn't look like a valid conflict