Skip to content

Commit

Permalink
conflicts: add "git-diff3" conflict marker style
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
scott2000 committed Nov 17, 2024
1 parent 6d918e5 commit 8dca1e1
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 12 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
78 changes: 67 additions & 11 deletions lib/src/conflicts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,23 @@ 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.
// TODO: All the `{7}` could be replaced with `{7,}` to allow longer
// 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<Regex> = 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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -281,6 +293,26 @@ pub fn materialize_merge_result(
Ok(())
}

fn materialize_git_diff3_conflict(
hunk: &Merge<BString>,
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<BString>,
conflict_marker_style: ConflictMarkerStyle,
Expand Down Expand Up @@ -452,31 +484,54 @@ pub fn parse_conflict(input: &[u8], num_sides: usize) -> Option<Vec<Merge<BStrin
}
}

// This method handles parsing both JJ-style and Git-style conflict markers,
// meaning that switching conflict marker styles won't prevent existing files
// with other conflict marker styles from being parsed successfully.
//
// If the hunk starts with a JJ-style conflict marker, then all Git-style
// markers will be ignored. Otherwise, it will be parsed as a Git-style
// conflict, and all JJ-style markers will be ignored.
fn parse_conflict_hunk(input: &[u8]) -> Merge<BString> {
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;
Expand Down Expand Up @@ -504,14 +559,15 @@ fn parse_conflict_hunk(input: &[u8]) -> Merge<BString> {
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
Expand Down

0 comments on commit 8dca1e1

Please sign in to comment.