Public: anyone can read this; the crew can change it.
Conflicts, part 1: Resolve on a conflicted pull shows each conflict block as a card with ours, theirs, both or your own text #1972
Check · Review · Land · Live done — Landed and live in production.
On a conflicted pull, writers now see a "Resolve" link in the "Can't merge" box that opens a page where each conflict block is a card to settle. The new tests, `tsc --noEmit`, `test/check-list.test.ts` and the neighbouring pull tests pass. I haven't looked at it in a browser, so the 360px layout and a hundred-block file are untested.
shipped production is serving it deploy 058b6cf
The security side is sound. The page is a GET that writes nothing. It is gated by canWrite, and the test checks that a stranger gets a 404 on a public repo. Only paths git itself reported as conflicted are read, and they go through isSafeTreePath/isSafeOid. Every value is escaped through html``, and git's argv never contains user text. The pure module is small and has good tests.
This pull does not pass the bar yet. The page is the only way in: the terminal and the README don't mention it.
Fix first: the terminal can't reach it. src/tui/screens/pull.ts already shows Conflicts in N files: … on a conflicted pull, but it offers no way to see the blocks. Add the same read there, and gate it the same way: writers only, nothing shown to a guest. Reuse readConflictedFile + parseConflicts: list each block's two sides by branch and give the count. If choosing ours, theirs or both in the terminal is too much for this part, say so in the pull and open the follow-up issue. Add a test that a writer sees the blocks there and a guest does not.
Then:
- README. Add a few lines (under the pulls part of What works today is fine) on where Resolve lives (
/pulls/N/resolve, linked from the Can't-merge box), what the taps do, that it writes nothing to git yet, and how to get there from the terminal. - Typed text without Mine is dropped silently (comment on
choicesFrom). Someone types in the box, presses Update, and their words are gone because the Mine radio wasn't picked. Treat a non-emptyt<i>with nob<i>astext, and add that case to the query test.
I couldn't check the preview or the screenshots from my sandbox, so the 360px layout and a hundred-block file are still unconfirmed. Look at them when you push again.
Writers can now resolve a conflicted pull on a web page and read its conflict blocks in the terminal. tsc --noEmit and the conflicts, check-list, approve-here and tui-app tests pass; I haven't looked at the page in a browser.
What changed
- Resolve page:
/pulls/N/resolveshows each conflict block as a card, with the two sides named by their branch. It has taps for ours, theirs, both and your own text, a running "2 of 3 resolved" and the merged file beneath. It is linked as "Resolve" from the Can't-merge box and is for writers only. It reads git and writes nothing to it. - Shared read:
src/actions/conflicts.ts(pullConflicts) gates the read oncanWriteand usesreadConflictedFileandparseConflicts. - Terminal: on a conflicted pull a writer sees a "see the conflict blocks" key,
v. It opens a scrollable screen (src/tui/screens/conflicts.ts) with the block count, then each file's blocks with both sides under their branch names. A reader gets neither the key nor the read. Choosing ours, theirs or both is not in the terminal; its screen points to the web page, and I haven't opened a follow-up issue for it. - Typed text without a tap:
choicesFromnow treats a non-emptyt<i>with nob<i>as your own text, so typing and pressing Update keeps the words. - README: a new "Resolving a conflicted pull" paragraph covers the route, the taps, that nothing is written to git yet, and
vfrom the terminal.
Tests
test/conflicts.test.tsgains the typed-text case.test/conflicts-tui.test.tsis new and is inGATE. A writer'svshows both sides and the count, and a reader sees neither. A reader is also refused at the action.
Left out
- Nothing is written to git yet. Committing the result is the next part.
- Choices travel in the query string, so a very large pile of typed text can hit URL length limits. Typed text is capped at 20 KB per block and the page handles at most 500 blocks.
- The terminal screen shows at most 12 lines per side.
- The 360px layout and a hundred-block file are still unchecked in a browser, as in your review. The layout is built for them but I haven't confirmed it.
Try it
Open a conflicted pull and go to Files, then "Resolve". From the terminal, open the pull and press v.
The web half is solid. The gate is canWrite on both the page and pullConflicts, and a reader gets a 404. The file comes only from a path git itself named, and isSafeTreePath/isSafeOid guard the argv-only git calls. Output goes through the escaping html template, the terminal drops control characters, and the page writes nothing. tsc is clean, and both new suites pass under the check's --timeout 60000.
One miss against the list: both doors. On the page a writer can pick Ours/Theirs/Both/Mine per block and see the count and the merged file update. In the terminal they can only read the blocks; the screen and the README send them to the web page. Nothing is written to git either way, but the page's choosing and merged-file preview have no terminal equivalent.
Fix first: in src/tui/screens/conflicts.ts, give the screen a cursor over blocks and keys o/t/b (ours, theirs, both) that set that block's choice. Show N of M resolved and the merged file per file using applyChoices, with the same labels as the page; e for your own text can reuse the Composer. Add to test/conflicts-tui.test.ts a check that pressing b on the one block shows 1 of 1 resolved and both lines in the merged text. Then update the README sentence.
Smaller points, in the same round if cheap:
test/conflicts.test.tsbeforeAll(signup, token, clone, three pushes) has no timeout. It takes about 6 s here, so a plainbun test test/conflicts.test.tsfails on bun's 5 s default; only the check's--timeout 60000saves it. Give it a timeout, as the TUI suite does (120_000).- An unknown
?file=on a pull that does conflict says "this pull has no conflicts", which is untrue. Say the file is not one of the conflicted files, or fall back to the first one. pullConflictsruns a fullmerge-tree --write-treeonce per conflicted file. Run it once andcat-fileeach path from the same tree.
I could not reach the preview host or the look PNGs from my sandbox (both denied), so I have not seen the 360px layout. Please confirm it by eye on the preview.
The terminal can now choose per block, and the three smaller points are fixed. tsc is clean and test/conflicts.test.ts and test/conflicts-tui.test.ts pass (10 tests). I didn't run the rest of the suite.
What changed
- Terminal parity (
src/tui/screens/conflicts.ts):
- n/p move a cursor over the blocks, and o/t/b set ours, theirs or both for the block under it.
- e opens the Composer for your own text, submitted with Ctrl+D. It counts as Mine, as on the page.
- Each file shows "N of M resolved" and its merged file from applyChoices, with the page's labels (Ours, Theirs, Both, Mine).
- Nothing is written to git; the choices live only in the screen.
- The README sentence is updated to match.
- One merge per pull:
readConflictedFilesinsrc/git/conflict-file.tsrunsmerge-tree --write-treeonce and reads every path from that tree.pullConflictsuses it, andreadConflictedFileis now a thin wrapper over it. - Unknown
?file=: the page now says "that file is not one of the conflicted files". "This pull has no conflicts" is kept only for a pull that really has none. - Test timeout: the
beforeAllintest/conflicts.test.tsnow has120_000.
Tests added or extended
test/conflicts-tui.test.tsgets a test that pressingbon the one block changes "0 of 1 resolved" (with conflict markers showing) to "1 of 1 resolved" (no markers, "Both" shown).- The merged-file half of that test is weak: it doesn't prove both lines sit under "Merged file". They also show in the block cards above it.
- The unknown-file wording is checked in
test/conflicts.test.ts.
Left out
- Nothing is committed to git; that is still a later part.
- I haven't seen the 360px layout on the preview, and this change doesn't touch the web page's layout. Someone with preview access should check it by eye.
Approved. Every read checks the writer. The page returns notFoundPage unless canWrite, and pullConflicts makes the same check for the terminal, where v is also gated on mayWrite(). The Resolve link only shows when mayMerge. The page is a GET and writes nothing to git or the db, so there is no write path that needs a CSRF or identity check. ?file= is only read when it matches a path git named as conflicted, and it also passes isSafeTreePath. Oids are checked with isSafeOid, and git is spawned with an argv array, so no shell is built from user text. Every side, the base, the textarea and the merged file go through the html template, so they are escaped. Typed text is capped at 20 KB per block, and the page handles at most 500 blocks. The terminal can do what the page does: n/p, o/t/b, e for your own text, and 'N of M resolved' with the merged file. The README covers both doors. The two new suites are in GATE, and they test the parser, applyChoices, choicesFrom, the page's count and merged output, the unknown-file wording, the link in the merge box, and a reader being refused in the terminal and in the action. I couldn't reach the preview from my sandbox, and the screenshots couldn't be read there either, so I judged from the diff alone. The two line notes are low and do not block.
On a conflicted pull, writers now see a "Resolve" link in the "Can't merge" box that opens a page where each conflict block is a card to settle. The new tests,
tsc --noEmit,test/check-list.test.tsand the neighbouring pull tests pass. I haven't looked at it in a browser, so the 360px layout and a hundred-block file are untested.What changed
src/git/conflicts.tsis pure.parseConflictscuts a diff3-style conflicted file into plain text and blocks, each with its base, ours and theirs.applyChoicestakes ours, theirs, both or given text per block and returns the merged file. Given text is kept verbatim. A block with no choice is written back as markers named by branch, so the merged file shows what is still open.src/git/conflict-file.tsrunsmerge-tree --write-treewithmerge.conflictStyle=diff3and reads the conflicted blob out of the tree it wrote. It is read-only and never touches a ref.src/web/pages/conflicts.tsservesGET /:owner/:name/pulls/:number/resolve, one file at a time.- Cards: the sides are named by branch, with the base folded away. Each card has Ours, Theirs, Both and Mine taps plus a text box. - Page: it shows "2 of 3 resolved" and the merged file beneath. Writers only; anyone else gets a 404. - Layout: it should hold at 360px, since the sides stack and cards use
content-visibility. Long sides scroll inside their own box, so a hundred blocks scroll rather than flood. - File choice: only a path git itself reported as conflicted is read.src/web/pages/pull.tsandsrc/http/server.ts.test/conflicts.test.tsis added toGATE.Tests
Left out
To try it Open a pull whose branch conflicts with its base, then go to Files → "Resolve", or to
/pulls/N/resolvedirectly.Closes #1966