merge-ll: consolidate conflict marker scanning logic

The diff.c:is_conflict_marker() and rerere.c:is_cmarker() functions implement duplicate logic for identifying conflict marker lines (lines that begin with a run of '<', '=', '>', and '|' characters). diff.c's original version from 049540435f (diff --check: detect leftover conflict markers, 2008-06-26) accepts any whitespace (such as a newline) immediately following '<<<<<<<' and '>>>>>>>', whereas rerere.c's version from 191f241717 (rerere: prepare for customizable conflict marker length, 2010-01-16) strictly requires a space character (' ') after them. Implement is_conflict_marker_line() in merge-ll.c to serve as a replacement for both, and update diff.c and rerere.c to use the new helper. The unified helper intentionally adopts rerere's stricter rule, as the conflicts generated by Git always show the "ours" and "theirs" labels after these markers separated by a space. Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Jul 31, 2026 at 05:56 UTC e28c701fe41825013220e1b9fefe8da038070aa2
4 files changed +39 -56
diff.c
+1 -24
@@ -3519,29 +3519,6 @@ struct checkdiff_t {
3519 int last_line_kind;
3520 };
3521
3522 -static int is_conflict_marker(const char *line, int marker_size, unsigned long len)
3523 -{
3524 - char firstchar;
3525 - int cnt;
3526 -
3527 - if (len < marker_size + 1)
3528 - return 0;
3529 - firstchar = line[0];
3530 - switch (firstchar) {
3531 - case '=': case '>': case '<': case '|':
3532 - break;
3533 - default:
3534 - return 0;
3535 - }
3536 - for (cnt = 1; cnt < marker_size; cnt++)
3537 - if (line[cnt] != firstchar)
3538 - return 0;
3539 - /* line[1] through line[marker_size-1] are same as firstchar */
3540 - if (len < marker_size + 1 || !isspace(line[marker_size]))
3541 - return 0;
3542 - return 1;
3543 -}
3544 -
3522 static void checkdiff_consume_hunk(void *priv,
3523 long ob UNUSED, long on UNUSED,
3524 long nb, long nn UNUSED,
@@ -3571,7 +3548,7 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)
3548 if (line[0] == '+') {
3549 unsigned bad;
3550 data->lineno++;
3574 - if (is_conflict_marker(line + 1, marker_size, len - 1)) {
3551 + if (is_conflict_marker_line(line + 1, len - 1, marker_size)) {
3552 data->status |= 1;
3553 fprintf(data->o->file,
3554 "%s%s:%d: leftover conflict marker\n",
merge-ll.c
+31
@@ -468,3 +468,34 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)
468 }
469 return marker_size;
470 }
471 +
472 +int is_conflict_marker_line(const char *line, unsigned long len, int marker_size)
473 +{
474 + char firstchar;
475 + int cnt;
476 +
477 + if (len < marker_size + 1)
478 + return 0;
479 +
480 + firstchar = line[0];
481 + switch (firstchar) {
482 + case '=': case '>': case '<': case '|':
483 + break;
484 + default:
485 + return 0;
486 + }
487 +
488 + for (cnt = 1; cnt < marker_size; cnt++) {
489 + if (line[cnt] != firstchar)
490 + return 0;
491 + }
492 +
493 + if (((firstchar == '<') || (firstchar == '>')) &&
494 + line[marker_size] != ' ')
495 + return 0;
496 +
497 + if (!isspace((unsigned char)line[marker_size]))
498 + return 0;
499 +
500 + return firstchar;
501 +}
merge-ll.h
+1
@@ -109,6 +109,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,
109 const struct ll_merge_options *opts);
110
111 int ll_merge_marker_size(struct index_state *istate, const char *path);
112 +int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);
113 void reset_merge_attributes(void);
114
115 #endif
rerere.c
+6 -32
@@ -331,33 +331,6 @@ static int rerere_file_getline(struct strbuf *sb, struct rerere_io *io_)
331 return strbuf_getwholeline(sb, io->input, '\n');
332 }
333
334 -/*
335 - * Require the exact number of conflict marker letters, no more, no
336 - * less, followed by SP or any whitespace
337 - * (including LF).
338 - */
339 -static int is_cmarker(char *buf, int marker_char, int marker_size)
340 -{
341 - int want_sp;
342 -
343 - /*
344 - * The beginning of our version and the end of their version
345 - * always are labeled like "<<<<< ours" or ">>>>> theirs",
346 - * hence we set want_sp for them. Note that the version from
347 - * the common ancestor in diff3-style output is not always
348 - * labelled (e.g. "||||| common" is often seen but "|||||"
349 - * alone is also valid), so we do not set want_sp.
350 - */
351 - want_sp = (marker_char == '<') || (marker_char == '>');
352 -
353 - while (marker_size--)
354 - if (*buf++ != marker_char)
355 - return 0;
356 - if (want_sp && *buf != ' ')
357 - return 0;
358 - return isspace(*buf);
359 -}
360 -
334 static void rerere_strbuf_putconflict(struct strbuf *buf, int ch, size_t size)
335 {
336 strbuf_addchars(buf, ch, size);
@@ -375,7 +348,8 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,
348 int has_conflicts = -1;
349
350 while (!io->getline(&buf, io)) {
378 - if (is_cmarker(buf.buf, '<', marker_size)) {
351 + int marker = is_conflict_marker_line(buf.buf, buf.len, marker_size);
352 + if (marker == '<') {
353 if (handle_conflict(&conflict, io, marker_size, NULL) < 0)
354 break;
355 if (hunk == RR_SIDE_1)
@@ -383,15 +357,15 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,
357 else
358 strbuf_addbuf(&two, &conflict);
359 strbuf_release(&conflict);
386 - } else if (is_cmarker(buf.buf, '|', marker_size)) {
360 + } else if (marker == '|') {
361 if (hunk != RR_SIDE_1)
362 break;
363 hunk = RR_ORIGINAL;
390 - } else if (is_cmarker(buf.buf, '=', marker_size)) {
364 + } else if (marker == '=') {
365 if (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)
366 break;
367 hunk = RR_SIDE_2;
394 - } else if (is_cmarker(buf.buf, '>', marker_size)) {
368 + } else if (marker == '>') {
369 if (hunk != RR_SIDE_2)
370 break;
371 if (strbuf_cmp(&one, &two) > 0)
@@ -442,7 +416,7 @@ static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_siz
416 git_hash_init(&ctx, the_hash_algo);
417
418 while (!io->getline(&buf, io)) {
445 - if (is_cmarker(buf.buf, '<', marker_size)) {
419 + if (is_conflict_marker_line(buf.buf, buf.len, marker_size) == '<') {
420 has_conflicts = handle_conflict(&out, io, marker_size,
421 hash ? &ctx : NULL);
422 if (has_conflicts < 0)