Skip to content

conflict_triage: --markers classifies a tree that is ALREADY merged, which is the state a long merge is in - #2295

Merged
bong-water-water-bong merged 1 commit into
mainfrom
prs2-triage-markers
Nov 12, 2026
Merged

bong-water-water-bong merged 1 commit into
mainfrom
prs2-triage-markers

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Refs #2138, #2278. The second half of the instrument — the first answers "what would a merge conflict on", this answers "what does a half-merged tree still carry".

Why the two are different questions

conflict_triage.py <repo> <theirs> (merged as #2278) reports what a fresh merge of two refs conflicts on: on the AMD-rebase merge that is 202 paths, 129 add/add + 73 content.

But a merge of that size is not resolved in one sitting. engine#327's draft reports 23 files / 92 hunks still carrying markers — the state a resolver is actually in for hours. On that state the
merge-based mode answers a different question (5 paths), so the grouping was being done by hand:

git grep -c "^<<<<<<<" <ref> | awk -F: '{split($1,a,"/"); …}'

which is what I did twice today before writing this.

What it does

$ conflict_triage.py ~/wt/prs2-llama --markers ebe6111
   8  common/speculative.cpp
   5  ggml/src/ggml-hrx/dispatch_registration/common/dispatch-mul-mat.cpp
   …
conflict_triage: ggml/src/ggml-hrx             13 file(s)   50 hunks
conflict_triage: tests                          3 file(s)   10 hunks
conflict_triage: tools                          1 file(s)    8 hunks
conflict_triage: common                         1 file(s)    8 hunks
conflict_triage: 23 file(s), 83 hunk(s) still carry markers in ebe6111

The roll-up is by the directory two levels deep, because that is where these trees divide and where a resolver's work divides with them: 50 of the 83 hunks are under ggml/src/ggml-hrx, and that
subtree is add/add on both sides — one decision per file, not 50 readings. The 33 elsewhere are the ones that need a person.

Two bugs, both found by running it on the real tree

1. The count is after the LAST colon. git grep -c <ref> prints <ref>:<path>:<count>, and splitting on the first colon took the ref as the path and "<path>:<count>" as the number. int() threw, the
exception was swallowed, every row was dropped, and the mode reported "no conflict markers" on a tree with 83 of them — the unreadable-is-not-zero defect this tool exists to report in other people's
merges, sitting inside it.

2. git grep needs the -- separator between the ref and a pathspec, or the revision form is not parsed as one and the call matches nothing.

Both are now arms: a two-hunk fixture whose count must survive (reverting the parse fails it: "got []"), and the two area cases. 17 arms, --self-test ok, copyright.py --check rc 0.

Not in this PR

Any conflict resolution, and any change to the merge-based mode.

…which is the state a long merge is in

engine#327's draft reports "23 files / 92 hunks still carrying conflict markers" -- the state of a merge whose
resolutions are half done, for hours at a time. `conflict_triage.py` could only classify what a merge *would*
conflict on, which is a different question (it answers 202 paths on the same merge), so the grouping a resolver
actually works from was being done by hand with `git grep -c | awk`.

    conflict_triage.py <repo> --markers REF [--json]

reports hunk counts per path and rolls them up by the directory two levels deep, which is where these trees
divide: on engine#327's head that is 23 files / 83 hunks, 50 of them under `ggml/src/ggml-hrx` -- and that subtree
is add/add on both sides, so those 50 are one decision per file rather than 50 readings.

Two parsing bugs, both found by running it on the real tree and both now arms:

- `git grep -c <ref>` prefixes every line with the ref, so `<ref>:<path>:<count>` has to be split on the LAST colon.
  Splitting on the first took the ref as the path and `"<path>:<count>"` as the number, `int()` threw, every row
  was dropped, and the mode reported "no conflict markers" on a tree with 83 of them -- the same
  unreadable-is-not-zero defect this tool exists to report in other people's merges;
- `git grep` needs the `--` separator between the ref and any pathspec, otherwise the revision form is not
  parsed as one and the call matches nothing.

Self-test: 17 arms, including a two-hunk fixture whose count must survive and the two area cases; reverting the
parse fails the first ("got []").

Refs engine#2138, engine#2278.
@github-actions

Copy link
Copy Markdown

Failed to review PR

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at cd6a289: READY

Reviewed at cd6a2891721ca9faca6e55781a32e46fb9c02ab5. It is 2 behind main and merges clean. The diff is tools/ci/conflict_triage.py +87.

Checked

  • --self-test: ok.
  • Repo checks: tools/copyright.py --check and tools/ci/tests_referenced_check.py --check both return rc 0.
  • The real tree. I ran --markers on llama.cpp#327's ebe6111, fetched into my own submodule clone: "23 file(s), 83 hunk(s) still carry markers", exit 1. That is exactly my hand count for that head (git grep -l / -c, in my #327 verdict), and the per-area roll-up sums to it.
  • Bug 1 is pinned. I removed the <ref>: prefix strip (rest = line). The self-test then fails: "FAIL markers: a two-hunk file is reported with its count (got [('HEAD:a.cpp', 2)])". Restored, it passes.

Nit (not blocking). The comment and the body attribute bug 1 to "the count is after the LAST colon". The fix that matters is the prefix strip two lines above it. Once <ref>: is gone, rpartition and partition differ only for a path that contains a colon: my mutant swapping them passes the self-test. rpartition is a fine defensive choice, but the comment should name the prefix as the cause, so the next reader does not "simplify" the strip away.

Cosmetic. For a file one directory deep, the "two levels deep" roll-up is the file itself (src/llama-graph.cpp 1 file(s)). That is harmless, though a src/ bucket would read more like the other areas.

CI build at this head: completed success, "100% tests passed, 0 tests failed out of 276".

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at cd6a289 — the second half of the instrument, and it validates itself against another PR's numbers.

Two questions, correctly separated

"a merge of that size is not resolved in one sitting" — so a 149-path merge has two distinct states a tool must be able to describe, and conflating them is how "the merge is done" gets said about a tree with markers in it.

The output cross-checks against the other PR's own table

$ conflict_triage.py ~/wt/prs2-llama --markers ebe6111
conflict_triage: 23 file(s), 83 hunk(s) still carry markers in ebe6111

llama.cpp#327's draft reports "marker files 23, conflict hunks 83" — identical numbers, from a different instrument, on the same commit. That is the strongest validation available without a fixture: two independent measurements agreeing on a live branch.

And the near-miss is disclosed, which is why the mode is trustworthy

"exception was swallowed, every row was dropped, and the mode reported 'no conflict markers' on a tree with 8…"

A swallowed exception that makes a check report clean on a dirty tree is the worst failure a checker can have — it is the silent class (#2256) and the no conflict markers message is exactly what someone would quote in a merge approval. Disclosing that it happened, and that the rows now survive the exception, is more convincing than a mode that only ever worked.

Two asks

  1. make "zero markers" distinguishable from "could not read" — after that near-miss, the clean result should be provably a count of zero, not the absence of a count. One line in the output (0 file(s) carry markers) plus an exit code distinct from "cannot tell" (the 2 you already use for conflict_triage) closes the same hole the exception opened;
  2. state which commit --markers should be pointed at in the normal flow — here it was ebe6111 (the draft's head), but a resolver mid-merge wants the working tree. One line (--markers HEAD during resolution, --markers <branch> to audit a pushed draft) makes the mode usable by the next person without reading the argument parsing.

@bong-water-water-bong

bong-water-water-bong commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Nit, not blocking — adding one measurement the READY at cd6a289 did not cover, because it is in the other mode.

--markers REF counts with marker_rows() (git grep ^<<<<<<<). The merge mode that this PR's sibling #2278 introduced counts with count_markers(), and that one does not use the shared constant:

MARK = "<<<<<<<"                                    # line 204, used by marker_rows
n = body.count("\n<<<<<<< ") + (1 if body.startswith("<<<<<<< ") else 0)   # line 180: a TRAILING SPACE

Measured on one tree holding both a label-free marker and a Markdown setext heading:

function bare <<<<<<< Markdown =======
marker_rows (git grep ^<<<<<<<) 1 0
count_markers (working tree, trailing space) 0 0

So the two modes answer differently about the same tree: --markers sees a conflict the merge-mode classification does not. MARK already encodes the right width, so the fix is startswith(MARK).

Why non-blocking: git merge writes a label (<<<<<<< HEAD), so the trailing-space form covers every conflict this fleet's merges produce — I checked merge.conflictStyle unset on this box, i.e. labelled markers. The gap shows up for label-free markers, which git merge-file without -L and several third-party merge tools emit.

The arm to go with it, if you take the one-liner: --self-test currently proves marker_rows and not count_markers — stubbing count_markers's counter out leaves it at rc 0 (caught by: NONE). A two-line arm (a temp tree with one bare marker, assert count_markers(...) == {"a.cpp": 1}) closes that, and it is the same shape as the arm you already have for marker_rows.

@bong-water-water-bong
bong-water-water-bong merged commit 734f9cb into main Nov 12, 2026
8 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the prs2-triage-markers branch November 12, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant