From 8dca1e191145a7c5f1d754b8d09ceaed2ceba9b2 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sun, 17 Nov 2024 08:00:04 -0600 Subject: [PATCH] conflicts: add "git-diff3" conflict marker style Adds a new "git-diff3" conflict marker style option. This option matches Git's "diff3" conflict style, allowing these conflicts to be parsed by some external tools that don't support JJ-style conflicts. If a conflict has more than 2 sides, then it falls back to the standard "diff" conflict marker style. The conflict parsing code now supports parsing Git-style conflict markers in addition to the normal JJ-style conflict markers, regardless of the conflict marker style setting. This has the benefit of allowing the user to switch the conflict marker style while they already have conflicts checked out, and their old conflicts will still be parsed correctly. It also allows styles to be mixed within the same file, which is possible if some conflicts in a file are 2-sided and others have more sides, since "git-diff3" will fall back to "diff" for some of the conflicts in the file. --- CHANGELOG.md | 4 ++- lib/src/conflicts.rs | 78 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 12 deletions(-) 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