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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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.

What changed

  • src/git/conflicts.ts is pure. parseConflicts cuts a diff3-style conflicted file into plain text and blocks, each with its base, ours and theirs. applyChoices takes 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.ts runs merge-tree --write-tree with merge.conflictStyle=diff3 and reads the conflicted blob out of the tree it wrote. It is read-only and never touches a ref.
  • src/web/pages/conflicts.ts serves GET /: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.

  • The link and the route are in src/web/pages/pull.ts and src/http/server.ts. test/conflicts.test.ts is added to GATE.

Tests

  • A two-block conflict resolved by "both" gives the expected file.
  • Text-box content is kept verbatim.
  • Unresolved and unclosed blocks behave as described above.
  • The page shows the count and the merged file.
  • A reader gets a 404, and a path git did not name is not read.
  • The Resolve link shows for writers.

Left out

  • Nothing is written to git. Committing the resolution is the next part.
  • The choices live in the query string, so the page keeps no state. A very large pile of typed text could hit URL length limits. Typed text is capped at 20 KB per block, and the page handles at most 500 blocks.
  • I didn't add a modify/delete or binary conflict view. Those files get a "resolve in your own checkout" note.
  • There's no JavaScript. "Update the merged file" is a plain GET form, so the count and the merged file refresh when you press it, not on each tap.

To try it Open a pull whose branch conflicts with its base, then go to Files → "Resolve", or to /pulls/N/resolve directly.

Closes #1966

Changes requested — before the latest push

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:

  1. 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.
  2. 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-empty t<i> with no b<i> as text, 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.

commented

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/resolve shows 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 on canWrite and uses readConflictedFile and parseConflicts.
  • 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: choicesFrom now treats a non-empty t<i> with no b<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 v from the terminal.

Tests

  • test/conflicts.test.ts gains the typed-text case.
  • test/conflicts-tui.test.ts is new and is in GATE. A writer's v shows 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.

Changes requested — before the latest push

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.ts beforeAll (signup, token, clone, three pushes) has no timeout. It takes about 6 s here, so a plain bun test test/conflicts.test.ts fails on bun's 5 s default; only the check's --timeout 60000 saves 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.
  • pullConflicts runs a full merge-tree --write-tree once per conflicted file. Run it once and cat-file each 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.

commented

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: readConflictedFiles in src/git/conflict-file.ts runs merge-tree --write-tree once and reads every path from that tree. pullConflicts uses it, and readConflictedFile is 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 beforeAll in test/conflicts.test.ts now has 120_000.

Tests added or extended

  • test/conflicts-tui.test.ts gets a test that pressing b on 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

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs