feat(prs): diff breakdown for each PR, shown in the header PR pill #50

Merged
chad merged 3 commits from feat/pr-breakdown into main 2026-11-06 21:13:34 -05:00
Collaborator

Ports sweatshop's PR breakdown. Every PR a thread opens now records how its diff splits across Generated / Tests / Docs / Migrations / Config / Code. The header PR pill shows the totals, and opens a list of every PR plus a combined category table.

What changed

  1. lib/diff-breakdown.ts (new, pure, tested). Ported from sweatshop: a git diff --numstat -z parser (renames keep the new path, binaries count 0), first-match-wins categories, and mergeBreakdowns. Categories are tuned to tansweat:
    • Generated: pnpm-lock.yaml, *.lock, **/routeTree.gen.ts, docs/vendor/**, *.snap
    • Tests: *.test.*, *.spec.*, __tests__/, fixtures/, *.fixture.*
    • Docs: docs/**, *.md, *.mdx
    • Migrations: no patterns. tansweat's SQL migrations live inline in server/db.ts, so matching that file would count real code as migrations. A comment says so, and empty categories don't show.
    • Config: *.config.*, package.json, pnpm-workspace.yaml, tsconfig*.json, .forgejo/**, Containerfile*, patches/**
    • Code: everything else
  2. create_pr records it. After the PR opens, a separate deps.once('diff-breakdown') step runs git diff --numstat -z origin/<base>...HEAD. <base> was already fetched in an earlier step, and a replay returns the recorded value. It's best-effort: if numstat fails, the result has no breakdown and the PR still succeeds. The result contract in docs/architecture/git.md is updated.
  3. lib/prs.ts. ThreadPR gains an optional breakdown, and title taken from the call's input.
  4. Header pill. After the existing state label it shows N files +A −D. Clicking opens a popover: one row per PR (live state dot, owner/repo#n, title, +/−, link), then the category table with a Total. A lone PR with no stats still links straight out. This adds components/ui/popover.tsx, the shadcn popover on the already-pinned radix-ui, so no new dependency.

Verification

  • pnpm typecheck and pnpm test pass (1331 passed, up from 1295). The new tests cover the parser, the categories, merging, create_pr with and without a numstat failure, and prsFromMessages reading title and breakdown.
  • Real numstat:
    • I ran the parser on git diff --numstat -z for the actual #47, #48 and #49 diffs. The totals match git --shortstat, and #48's split matches Forgejo's own breakdown (Docs 1 +3, Code 27 +97 −61).
    • A probe repo with a rename, a binary file, a lockfile and a vendored doc parsed correctly. The rename kept its new path and was filed as Tests.
  • Sandbox output: the docker/podman sandbox decodes stdout by concatenating the raw bytes, which keeps the NUL separators. I checked that with the same decode on real git output. The git() helper's .trim() only strips the trailing NUL.
  • Browser: I rendered the pill with #47–#49's real stats plus an older PR without stats (screenshots below):
    • The pill showed 4 PRs and 43 files +642 −118.
    • The table read Tests 2 / Docs 3 / Code 38, with the older PR falling back to its branch name.

Limits

  • Only PRs opened after this ships get stats. Older ones keep their row and live state, without numbers.
  • Not yet run through a live create_pr. The first real PR will be the end-to-end check.
  • Breakdown on the approval card is a planned follow-up, not in this PR.

Screenshots

PR popover, light

PR popover, dark

Ports sweatshop's PR breakdown. Every PR a thread opens now records how its diff splits across Generated / Tests / Docs / Migrations / Config / Code. The header PR pill shows the totals, and opens a list of every PR plus a combined category table. ## What changed 1. **`lib/diff-breakdown.ts` (new, pure, tested).** Ported from sweatshop: a `git diff --numstat -z` parser (renames keep the new path, binaries count 0), first-match-wins categories, and `mergeBreakdowns`. Categories are tuned to tansweat: - **Generated:** `pnpm-lock.yaml`, `*.lock`, `**/routeTree.gen.ts`, `docs/vendor/**`, `*.snap` - **Tests:** `*.test.*`, `*.spec.*`, `__tests__/`, `fixtures/`, `*.fixture.*` - **Docs:** `docs/**`, `*.md`, `*.mdx` - **Migrations:** no patterns. tansweat's SQL migrations live inline in `server/db.ts`, so matching that file would count real code as migrations. A comment says so, and empty categories don't show. - **Config:** `*.config.*`, `package.json`, `pnpm-workspace.yaml`, `tsconfig*.json`, `.forgejo/**`, `Containerfile*`, `patches/**` - **Code:** everything else 2. **`create_pr` records it.** After the PR opens, a separate `deps.once('diff-breakdown')` step runs `git diff --numstat -z origin/<base>...HEAD`. `<base>` was already fetched in an earlier step, and a replay returns the recorded value. It's best-effort: if numstat fails, the result has no `breakdown` and the PR still succeeds. The result contract in `docs/architecture/git.md` is updated. 3. **`lib/prs.ts`.** `ThreadPR` gains an optional `breakdown`, and `title` taken from the call's input. 4. **Header pill.** After the existing state label it shows `N files +A −D`. Clicking opens a popover: one row per PR (live state dot, `owner/repo#n`, title, +/−, link), then the category table with a Total. A lone PR with no stats still links straight out. This adds `components/ui/popover.tsx`, the shadcn popover on the already-pinned `radix-ui`, so no new dependency. ## Verification - `pnpm typecheck` and `pnpm test` pass (1331 passed, up from 1295). The new tests cover the parser, the categories, merging, `create_pr` with and without a numstat failure, and `prsFromMessages` reading title and breakdown. - **Real numstat:** - I ran the parser on `git diff --numstat -z` for the actual #47, #48 and #49 diffs. The totals match `git --shortstat`, and #48's split matches Forgejo's own breakdown (Docs 1 +3, Code 27 +97 −61). - A probe repo with a rename, a binary file, a lockfile and a vendored doc parsed correctly. The rename kept its new path and was filed as Tests. - **Sandbox output:** the docker/podman sandbox decodes stdout by concatenating the raw bytes, which keeps the NUL separators. I checked that with the same decode on real git output. The `git()` helper's `.trim()` only strips the trailing NUL. - **Browser:** I rendered the pill with #47–#49's real stats plus an older PR without stats (screenshots below): - The pill showed 4 PRs and 43 files +642 −118. - The table read Tests 2 / Docs 3 / Code 38, with the older PR falling back to its branch name. ## Limits - Only PRs opened after this ships get stats. Older ones keep their row and live state, without numbers. - Not yet run through a live `create_pr`. The first real PR will be the end-to-end check. - Breakdown on the approval card is a planned follow-up, not in this PR. ## Screenshots ![PR popover, light](./pr-popover-light.png) ![PR popover, dark](./pr-popover-dark.png)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
feat(ui): PR pill popover with per-PR rows and diff breakdown table
All checks were successful
build / build-and-test (pull_request) Successful in 2m40s
build / build-sandbox-image (pull_request) Successful in 1s
build / deploy (pull_request) Has been skipped
cb42fef1a6
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
chad merged commit b07776db07 into main 2026-11-06 21:13:34 -05:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chad/tansweat!50
No description provided.