Files
kandev/docs/plans/pr-only-commit-details/plan.md
T
2026-08-24 11:09:03 +00:00

220 lines
10 KiB
Markdown

---
spec: docs/specs/ui/requirements/pr-only-commit-details.md
created: 2026-08-04
status: implemented
---
# Implementation Plan: Repair PR-only commit details
## Overview
Make commit provenance a first-class part of the Changes panel. Preserve the
local session path for SHAs known to the worktree, but route PR-only SHAs to a
new workspace-authorized GitHub individual-commit action. Hide unmeasured row
statistics and local mutation controls for remote commits, then cover the
force-pushed/stale-worktree case on desktop and mobile.
## Confirmed root cause
For task `8531031a-9a98-439f-ab0a-c25b480158dc`, the stored session worktree
was created at local head `5c2873d`, while linked PR #2253 was later
force-pushed to six different commits ending at `df3c05e`.
- `mergeCommits` in
`apps/web/components/task/changes-panel-helpers.ts` appends PR-only SHAs to
the local commit feed without preserving their source.
- GitHub's pull request commit-list response does not include commit stats.
`convertRawPRCommits` leaves Go integer fields at zero, so the frontend
presents unknown values as `+0 -0`.
- Every row click is reduced to a SHA and repository name, then sent through
`session.commit_diff`. The backend runs `git show` in the task worktree even
for PR-only SHAs.
- The shared row also exposes local reset/revert/amend callbacks for PR-only
commits, while `CommitDetailPanel` resolves metadata only from local session
commits.
The repair invariant is: row statistics, detail data, metadata, and available
actions must all derive from the same explicit commit source.
## Backend
### Workspace-scoped GitHub commit detail
- Add a `github.pr_commit.get` WebSocket action in
`apps/backend/pkg/websocket/actions.go` and register it in
`apps/backend/internal/github/handlers.go`.
- Add `StatsAvailable bool` with JSON field `stats_available` to `PRCommitInfo`.
Define `PRCommitDetail` in `apps/backend/internal/github/models.go` with SHA,
message, author login/name/date, exact additions/deletions, files changed,
and the merged `[]PRFile` returned by the individual GitHub commit endpoint.
- Extend the GitHub client interface and the `gh`, PAT, noop, and mock clients
with an exact-SHA commit-detail method. The CLI client should call
`gh api repos/{owner}/{repo}/commits/{sha}`; the PAT client should call the
equivalent REST path. Parse both through one helper so their response
semantics stay aligned.
- Request the maximum supported page size and merge every returned file page
for both clients (`gh api --paginate --slurp` for the CLI representation and
Link-header pagination for PAT). Preserve the first page's metadata/stats,
append file records in provider order without duplication, and test a
multi-page response.
- Add a service method that reuses `ensureRepositoryInWorkspaceScope` and the
existing personal read-client resolution. Validate that the SHA is a safe
exact object ID before interpolating it into either client path.
- Do not fetch, persist, or import the commit into the task worktree.
### Unknown list statistics
- Use `PRCommitInfo.stats_available` to explicitly represent whether additions,
deletions, and files-changed values are available. The normal GitHub list
parser must set it to false rather than relying on zero values.
- Keep exact integers mandatory on `PRCommitDetail`, where the individual
endpoint provides them. This lets a genuinely empty commit remain
distinguishable from an unmeasured row.
## Frontend
### Source-aware commit targets
- Introduce a discriminated commit-detail target: local targets contain the
existing SHA/repository routing data; GitHub targets additionally contain
workspace, owner, and repository identity.
- Make `mergeCommits` return that target plus a statistics-availability flag.
A SHA found in both feeds remains one local row and retains its pushed state.
A PR-only SHA becomes a GitHub target and does not acquire zero statistics.
- Thread the full target through the Changes panel callbacks, desktop dockview
panel params/actions, and mobile diff target. Include source and repository
identity in dockview panel identity so equal SHAs from different repositories
cannot collide.
### Unified detail loading and rendering
- Add a source-aware commit-detail hook/request layer. Local targets continue
to use `session.commit_diff`; GitHub targets call `github.pr_commit.get` and
map `PRFile` records into the current diff-view model.
- Have `CommitDetailPanel` obtain metadata and files from that unified result,
fixing the missing PR-only header as well as the patch source.
- For GitHub targets, disable worktree-dependent file actions and local context
expansion. Preserve the current rendering for available, binary, empty, or
truncated GitHub patches; never retry through the local git path.
- Gate reset/revert/amend row actions on local provenance. Render additions and
deletions only when the row marks them as measured.
## Desktop and mobile behavior
The visual composition does not change. Desktop continues to open the existing
dockview commit panel. Mobile continues to use
`MobileChangesPanel` -> `MobileDiffSheet`, the current full-height drawer and
closest native-mobile exemplar. Both surfaces consume the same discriminated
target and detail hook. The mobile drawer keeps its existing single scroll
owner, close affordance, and touch behavior; only its data source and local
action eligibility change.
## Tests
- Backend parser tests prove list statistics are unavailable and individual
commit responses preserve exact metadata, stats, file status, and patch.
- `gh` and PAT client tests prove the exact repository/SHA path and shared
response parsing.
- Handler/service tests prove workspace/repository authorization, exact-SHA
validation, response routing, and error propagation.
- Frontend merge tests prove PR-only provenance, hidden unknown stats,
multi-repository identity, and local precedence for a SHA found in both
feeds.
- Request/hook tests prove mutually exclusive local and GitHub actions and no
local fallback after a GitHub error.
- Panel/row tests prove remote metadata is visible and reset/revert/amend,
worktree-file actions, and local expansion are absent for GitHub targets.
- Dockview and mobile target tests prove source/repository identity survives
serialization and selection.
All production changes follow Red-Green-Refactor: add each focused regression,
record the expected failure against current behavior, add the smallest source
change, then rerun the focused and package-level checks.
## E2E tests
Extend `apps/web/e2e/tests/git/git-changes-panel.spec.ts` with a linked PR whose
mocked current commits are absent from the stale local worktree. Assert that the
row omits false `+0 -0`, its local mutation menu is absent, and opening it shows
the GitHub-specific commit header and patch marker. The scenario must fail if
the UI invokes `session.commit_diff` for that SHA.
Extend `apps/web/e2e/tests/task/mobile-changes-panel.spec.ts` with the same
PR-only source. Assert that tapping the row opens the existing full-height
commit sheet, renders the remote marker patch, exposes no local action, closes
normally, and introduces no horizontal overflow. Add deterministic mock GitHub
individual-commit seeding to the existing mock controller/API helper rather
than depending on live GitHub.
## Implementation waves and parallel candidates
Wave 1 (sequential):
- [x] [Task 01: GitHub commit detail contract](task-01-github-commit-detail.md)
— add the authenticated individual-commit backend contract and distinguish
unknown list statistics.
Wave 2 (depends on Wave 1):
- [x] [Task 02: Source-aware commit UI](task-02-source-aware-commit-ui.md) —
route local and GitHub targets correctly across desktop and mobile.
Wave 3 (depends on Waves 1-2):
- [x] [Task 03: Commit-source E2E coverage](task-03-commit-source-e2e.md) —
exercise stale local history against deterministic remote commit details.
The tasks are intentionally sequential. Task 02 consumes the backend wire
contract from Task 01, and Task 03 validates the integrated behavior and adds
mock support for that settled contract.
## Verification
Run the focused commands recorded in each task, then finish with:
```bash
cd apps/backend && go test ./internal/github
cd apps/web && pnpm run typecheck
cd apps && pnpm --filter @kandev/web lint
cd apps/web && pnpm run i18n:check && pnpm run i18n:ratchet
cd apps/web && pnpm e2e:run tests/git/git-changes-panel.spec.ts -- --grep "PR-only commit"
cd apps/web && pnpm e2e:run --project mobile-chrome tests/task/mobile-changes-panel.spec.ts -- --grep "PR-only commit"
git diff --check
```
## Verification results
- Backend: `go test ./internal/github` passed (1081 tests).
- Frontend: focused source/target suite passed (7 files, 72 tests), followed by
`pnpm run typecheck`, web lint, `i18n:check`, and `i18n:ratchet`.
- E2E: the desktop PR-only scenario passed once; the mobile scenario passed
once after narrowing an assertion to the opened sheet. Both scenarios cover
stale local history, hidden unknown statistics/actions, remote metadata, and
remote patch rendering.
- `git diff --check` passed.
- Review remediation: complete PR identity now gates visible commit-list
results; unavailable or malformed detail responses show a persistent retry
state; and remote detail loading is independent of local agent readiness.
The broader focused frontend suite passed (8 files, 69 tests), plus the
remediation request/hook/panel tests (4 files, 15 tests), typecheck, lint,
i18n checks, and ratchet.
- Review follow-up after rebasing onto `main`: root repository identity is now
matched exactly, invalid/null commit payloads are rejected at both backend
and frontend boundaries, target switches cannot expose prior detail state,
and serialized GitHub targets require complete identity. Backend GitHub tests
passed (1,086); focused frontend tests passed (8 files, 73 tests), with
typecheck, lint, i18n checks, ratchet, and diff checks green.
## Risks and out of scope
- Individual GitHub commit requests add a lazy network round trip and can be
rate-limited. The existing loading/error treatment must remain responsive,
and no eager N+1 list loading is allowed.
- GitHub can omit patches for binary or oversized changes. That state must be
presented honestly and must not trigger a local-data fallback.
- The source target becomes part of desktop panel identity and mobile
selection state. Tests must prevent stale serialized targets or
cross-repository SHA collisions.
- No database migration, public documentation change, worktree synchronization,
or non-GitHub provider is part of this repair.