Handle jj diff-style conflict markers in autoupdate (#6559)

This commit is contained in:
Geoff Romer
2026-01-07 17:17:40 +00:00
committed by GitHub
parent 3a7c44c5c4
commit 2e59a0d520
3 changed files with 285 additions and 58 deletions
+98 -57
View File
@@ -25,75 +25,116 @@ using ::testing::Matcher;
using ::testing::MatchesRegex;
using ::testing::StrEq;
// Processes conflict markers, including tracking of whether code is within a
// conflict marker. Returns true if the line is consumed.
// Represents the different kinds of version-control conflict markers that are
// relevant for the autoupdater. One key concern here is the distinction between
// "snapshot" and "diff" conflict regions. Snapshot regions are the more
// traditional kind, where the entire region between two markers represents the
// exact state of a region of the underlying file at some snapshot (e.g. the
// base commit or one of the conflicting commits). Diff regions are
// produced by jj. They show the diff between the base and one side of the
// conflict, using a prefix character on each line: '+' indicates an added line,
// '-' indicates a removed line, and ' ' indicates an unchanged line. Note that
// a single conflict may contain both snapshot and diff regions.
//
// See https://docs.jj-vcs.dev/latest/conflicts/ for more information.
enum class MarkerKind {
// Represents a line that is not a conflict marker.
None,
// Marks the start of a conflict, and potentially a snapshot region.
Start,
// Marks the end of a conflict.
End,
// Marks the start of a snapshot region.
Snapshot,
// Marks the start of a diff region.
Diff
};
// Processes conflict markers, including tracking the previous conflict marker.
// Returns true if the line is consumed.
static auto TryConsumeConflictMarker(bool running_autoupdate,
llvm::StringRef line,
llvm::StringRef line_trimmed,
bool& inside_conflict_marker)
MarkerKind& previous_marker)
-> ErrorOr<bool> {
bool is_start = line.starts_with("<<<<<<<");
bool is_end = line.starts_with(">>>>>>>");
bool is_middle =
// git internal conflict markers ("merge" and "diff3" style).
line.starts_with("=======") ||
line.starts_with("|||||||")
// jj internal conflict markers ("snapshot" style).
|| line.starts_with("+++++++") || line.starts_with("-------");
// jj internal conflict marker ("diff" style)
bool is_jj_diff = line.starts_with("%%%%%%%");
MarkerKind new_marker;
if (line.starts_with("<<<<<<<")) {
new_marker = MarkerKind::Start;
} else if (line.starts_with(">>>>>>>")) {
new_marker = MarkerKind::End;
} else if (line.starts_with("=======") || line.starts_with("|||||||") ||
line.starts_with("+++++++") || line.starts_with("-------")) {
// git uses "=======" and "|||||||" to mark boundaries between conflict
// regions (which are always snapshots). jj uses "+++++++" and "-------" to
// mark the start of different kinds of snapshot regions.
new_marker = MarkerKind::Snapshot;
} else if (line.starts_with("%%%%%%%") || line.starts_with(R"(\\\\\\\)")) {
// jj uses "%%%%%%%" to mark the start of a diff region, and "\\\\\\\" to
// add a second line to a "%%%%%%%" marker for formatting purposes.
new_marker = MarkerKind::Diff;
} else {
new_marker = MarkerKind::None;
}
// When running the test, any conflict marker is an error.
if (!running_autoupdate && (is_start || is_middle || is_end || is_jj_diff)) {
if (!running_autoupdate && (new_marker != MarkerKind::None)) {
return ErrorBuilder() << "Conflict marker found:\n" << line;
}
if (is_jj_diff && running_autoupdate) {
// TODO: Add support for JJ's diff-style conflict markers.
return ErrorBuilder()
<< "Found jj \"diff\" style conflict marker."
" Autoupdate only supports \"snapshot\" style conflict markers."
" To switch, use `jj config set --repo ui.conflict-marker-style"
" \"snapshot\"`, and then run `jj new` (or `jj edit`) again to "
" materialize the new style. For more details, see: "
"https://docs.jj-vcs.dev/latest/conflicts/";
}
bool inside_conflict_marker = [&] {
switch (previous_marker) {
case MarkerKind::None:
case MarkerKind::End:
return false;
case MarkerKind::Start:
case MarkerKind::Snapshot:
case MarkerKind::Diff:
return true;
}
}();
// Autoupdate tracks conflict markers for context, and will discard
// conflicting lines when it can autoupdate them.
if (inside_conflict_marker) {
if (is_start) {
return ErrorBuilder() << "Unexpected conflict marker inside conflict:\n"
<< line;
}
if (is_middle) {
switch (new_marker) {
case MarkerKind::End:
case MarkerKind::Snapshot:
case MarkerKind::Diff:
if (!inside_conflict_marker) {
return ErrorBuilder()
<< "Unexpected conflict marker outside conflict:\n"
<< line;
}
previous_marker = new_marker;
return true;
}
if (is_end) {
inside_conflict_marker = false;
case MarkerKind::Start:
if (inside_conflict_marker) {
return ErrorBuilder() << "Unexpected conflict marker inside conflict:\n"
<< line;
}
previous_marker = new_marker;
return true;
}
case MarkerKind::None:
if (!inside_conflict_marker) {
return false;
}
// Look for CHECK and TIP lines, which can be discarded.
if (line_trimmed.starts_with("// CHECK:STDOUT:") ||
line_trimmed.starts_with("// CHECK:STDERR:") ||
line_trimmed.starts_with("// TIP:")) {
return true;
}
if (previous_marker == MarkerKind::Diff) {
if (!line.consume_front(" ") && !line.consume_front("+") &&
!line.consume_front("-")) {
return ErrorBuilder() << "Line inside diff-style conflict doesn't "
"start with '+', '-', or ' ':\n"
<< line;
}
line_trimmed = line.ltrim();
}
return ErrorBuilder()
<< "Autoupdate can't discard non-CHECK lines inside conflicts:\n"
<< line;
} else {
if (is_start) {
inside_conflict_marker = true;
return true;
}
if (is_middle || is_end) {
return ErrorBuilder() << "Unexpected conflict marker outside conflict:\n"
<< line;
}
return false;
// Look for CHECK and TIP lines, which can be discarded.
if (line_trimmed.starts_with("// CHECK:STDOUT:") ||
line_trimmed.starts_with("// CHECK:STDERR:") ||
line_trimmed.starts_with("// TIP:")) {
return true;
}
return ErrorBuilder() << "Autoupdate can't discard non-CHECK lines "
"inside conflicts:\n";
}
}
@@ -718,7 +759,7 @@ static auto ProcessFileContent(llvm::StringRef filename,
// When autoupdating, we track whether we're inside conflict markers.
// Otherwise conflict markers are errors.
bool inside_conflict_marker = false;
auto previous_conflict_marker = MarkerKind::None;
SplitState split_state;
@@ -732,7 +773,7 @@ static auto ProcessFileContent(llvm::StringRef filename,
CARBON_ASSIGN_OR_RETURN(
is_consumed,
TryConsumeConflictMarker(running_autoupdate, line, line_trimmed,
inside_conflict_marker));
previous_conflict_marker));
if (is_consumed) {
continue;
}