feat(diff): diff a whole branch vs a base (?base=<rev>) — review before landing (W3)
The git-diff viewer can now diff the current branch against a base commit-ish
(e.g. main) — review an agent's whole branch from your phone before merging, not
just uncommitted changes. Completes the long-deferred FR-B1.9.
- src/http/diff.ts: getDiff() gains an optional base. Three-layer defense so an
attacker-supplied base never reaches a shell or acts as a git option:
(1) isPlausibleRev() rejects leading '-', '..'/'...' ranges, metachars, control
chars, >250 chars; (2) git rev-parse --verify --quiet --end-of-options
<base>^{commit} — only a resolved 7-64 hex sha is accepted, else empty result;
(3) git diff --no-color <sha>... -- (three-dot = the branch's changes since
divergence, PR-style). execFile, no shell, timeout + maxBuffer bound.
- src/types.ts: additive optional base on DiffResult.
- src/server.ts GET /projects/diff reads ?base (400 on !isPlausibleRev); read-only.
- public/diff.ts: a "compare base" <select> (Working tree + one option per base),
disables Working/Staged tabs in base mode. public/projects.ts derives bases from
the worktree branches ∪ current branch.
Verified: typecheck + build:web clean, 1763 pass (diff tests 87 green). The 1 red
in the plain full run is the pre-existing real-PTY "ring buffer" flake (times out
at default 5s under sandbox load; 27/27 at --test-timeout=30000) — unrelated.
This commit is contained in:
@@ -39,7 +39,7 @@ import { deriveApprovalPreview } from './http/approval-preview.js'
|
||||
import { listSessions } from './http/history.js'
|
||||
import { buildProjects, buildProjectDetail } from './http/projects.js'
|
||||
import { openInEditor, openFileInEditor } from './http/editor.js'
|
||||
import { getDiff } from './http/diff.js'
|
||||
import { getDiff, isPlausibleRev } from './http/diff.js'
|
||||
import { parseStatusLine } from './http/statusline.js'
|
||||
import { createWorktree } from './http/worktrees.js'
|
||||
import { createSessionManager } from './session/manager.js'
|
||||
@@ -801,9 +801,18 @@ export function startServer(cfg: Config): { close(): Promise<void> } {
|
||||
res.status(404).json({ error: 'project not found' }) // SEC-H7 three-prong
|
||||
return
|
||||
}
|
||||
// FR-B1.9: optional ?base=<rev> — diff HEAD against a base commit-ish. The
|
||||
// rev-parse allow-list (in getDiff) is the real defense; isPlausibleRev
|
||||
// rejects flag-injection/junk fast with a 400 before any git call.
|
||||
const rawBase = req.query['base']
|
||||
const base = typeof rawBase === 'string' && rawBase !== '' ? rawBase : undefined
|
||||
if (base !== undefined && !isPlausibleRev(base)) {
|
||||
res.status(400).json({ error: 'invalid base revision' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const staged = req.query['staged'] === '1'
|
||||
res.json(await getDiff(target, { staged, cfg }))
|
||||
res.json(await getDiff(target, { staged, base, cfg }))
|
||||
} catch (err) {
|
||||
console.error('[server] /projects/diff failed:', err instanceof Error ? err.message : String(err))
|
||||
res.status(500).json({ error: 'failed to read diff' })
|
||||
|
||||
Reference in New Issue
Block a user