From ac4a13a5e9eec49996b3e2bbc323931191541149 Mon Sep 17 00:00:00 2001 From: v0lumehi Date: Tue, 27 Oct 2026 12:02:48 +0100 Subject: [PATCH] Merge docs/reference and the symptom index with git's union driver About four in five new reference entries go at the end of their file (437 of 551 new ### entries in the last 400 commits to docs/reference), and docs/agents/symptoms.md is generated from them line for line. So two pull requests that each add an entry to the same file, rendering.md or shaders.md most often, conflict twice: in the reference file and in the index, both at the end of the same group. .gitattributes sets merge=union for docs/reference/*.md and docs/agents/symptoms.md. Git's built-in union driver keeps both sides' new lines; it needs no setup and applies to merges, rebases and cherry-picks. Checked in a clone: two branches that each append an entry to rendering.md and regenerate the index rebase onto each other without a conflict, the index is current afterwards (docs_symptoms.py --check), and without the attribute the same rebase conflicts in both files. Where both sides change the same line, the union driver keeps both versions without a conflict. test_docs now fails on an entry with a Symptom, Cause, Fix or Found on bullet more than once, or a heading twice in one file: checked with two branches that each add a game to the same Found on line, which merge into two Found on lines that the test reports. The seven entries that already have a bullet twice are listed as the baseline, and a count there may only go down. --- .gitattributes | 5 +++++ dm/tools/tests/test_docs.py | 38 ++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 8fa2fa7865..b96623cfc7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,8 @@ # The games gallery's table (dm/tools/docs_gallery.py writes README.md's gallery from it): a new game is a row at its end. README.md itself is not merged this way; after a # conflict in its gallery, run docs_gallery.py. test_docs fails on a game in the table twice docs/images/gallery.tsv merge=union +# Findings are added as new entries, mostly at the end of a reference file, and the symptom index is generated from them line for line. Two pull requests that each add an +# entry to the same file conflicted there and in docs/agents/symptoms.md every time. The union merge keeps both sides' new lines instead. Where both sides change the same +# line, it keeps both versions without a conflict: dm/tools/tests/test_docs.py fails on an entry with a bullet twice, and docs_symptoms.py --check on an index out of order. +docs/reference/*.md merge=union +docs/agents/symptoms.md merge=union diff --git a/dm/tools/tests/test_docs.py b/dm/tools/tests/test_docs.py index af73c08784..3f71719908 100644 --- a/dm/tools/tests/test_docs.py +++ b/dm/tools/tests/test_docs.py @@ -1,7 +1,7 @@ """The repository's own Markdown stays navigable: every relative link and #anchor resolves (GitHub's heading anchors), every game profile has its note (dm/games//README.md) and a row in the games table of dm/games/README.md, the symptom index (docs/agents/symptoms.md) is the one dm/tools/docs_symptoms.py writes from docs/reference, the patch index (docs/agents/game-patches.md) is the one dm/tools/docs_patches.py writes from the patch files, host tables and profiles, and the entry points stay short: AGENTS.md at most 1,500 words, each numbered guide in docs/agents at most 6,000 (a new finding goes into a game note or a -reference, not into these), and README's games gallery is the one dm/tools/docs_gallery.py writes from docs/images/gallery.tsv. Third-party trees are left out. No game files needed.""" +reference, not into these), README's games gallery is the one dm/tools/docs_gallery.py writes from docs/images/gallery.tsv, and no reference entry has a bullet twice (the guard of .gitattributes' union merge). Third-party trees are left out. No game files needed.""" from pathlib import Path import re import subprocess @@ -174,6 +174,42 @@ def test_union_merge_rejects_conflicting_game_rows(self): docs_gallery.readme() +# Entries that already have a bullet more than once (count of the bullet in that entry); .gitattributes merges docs/reference with the union driver, which keeps both versions +# of a line two branches changed, so an entry gaining a second bullet is what a bad union merge looks like. A count here may only go down +BULLETS_TWICE = { + ('frame-rate.md', "Present snapshots: the game's swapchain texture free at once", 'Fix'): 3, + ('rendering.md', 'Pulling vertex inputs', 'Cause'): 2, + ('rendering.md', 'Pulling vertex inputs', 'Fix'): 2, + ('rendering.md', 'Transform feedback captured by a vertex entry point', 'Found on'): 2, + ('services.md', 'oe calls answered without an applet proxy', 'Symptom'): 2, + ('services.md', 'oe calls answered without an applet proxy', 'Fix'): 2, + ('shaders.md', 'Passthrough geometry shaders drawn by the vertex shader', 'Fix'): 3, + ('shaders.md', 'Passthrough geometry shaders drawn by the vertex shader', 'Found on'): 3, + ('threads.md', 'A host lock held across a same-core handoff', 'Found on'): 2, +} + + +class UnionMerge(unittest.TestCase): + def test_reference_entries_have_each_bullet_once(self): + bad = [] + for path in sorted((REPO / 'docs/reference').glob('*.md')): + head, fence, heads, count = None, False, {}, {} + for line in path.read_text().split('\n'): + if line.startswith('```'): fence = not fence + if fence: continue + m = re.match(r'### (.*?)\s*$', line) + if m: + head = m.group(1); heads[head] = heads.get(head, 0) + 1 + if heads[head] > 1: bad.append(f'{path.name}: heading "{head}" twice') + continue + if re.match(r'#{1,2} ', line): head = None + m = re.match(r'- \*\*(Symptom|Cause|Fix|Found on):\*\*', line) + if m and head: + k = (path.name, head, m.group(1)); count[k] = count.get(k, 0) + 1 + bad += [f'{f}: "{h}": {b} {n} times' for (f, h, b), n in count.items() if n > BULLETS_TWICE.get((f, h, b), 1)] + self.assertEqual(bad, [], 'a reference entry has a bullet or heading more than once (a union merge kept both versions of a line? merge them by hand):\n' + '\n'.join(bad)) + + class Length(unittest.TestCase): def test_entry_points_stay_short(self): words = lambda p: len(p.read_text().split())