feat: add --staged diff scope for pre-commit hook scanning (#18)#53
feat: add --staged diff scope for pre-commit hook scanning (#18)#53ChrisJr404 wants to merge 1 commit into
Conversation
Adds a --staged flag to `openant scan` and `openant diff` that runs the incremental pipeline against the staged index (`git diff --cached`) instead of a committed ref. This is the workflow described by the DiffGuard project referenced in knostic#18: scan only what's about to be committed, before commit. Implementation: - New BuildStagedManifest, StagedChangedFiles, StagedHunksForFile in internal/git/diff.go. The manifest's base_ref is the literal "STAGED", base_sha is HEAD, and head_sha is a zero placeholder (the index has no SHA). The Python core reads these as opaque strings, so reports show "base=STAGED" without any core changes. - diffOpts grows a staged bool; validate() makes --staged mutually exclusive with --diff-base/--pr. - modeOpts/modeDecision thread the staged choice through selectMode so scan, diff, and the meta.json running state all agree. - README gains a short "Incremental and diff-based scans" section listing the four scopes (--diff-base, --pr, --staged, --incremental). Untracked files are not in the index, so they don't appear in the staged manifest. Pre-commit hooks usually just `git add` first, which is the standard expectation; we don't try to second-guess that. Tests: - 5 new git pkg tests: staged file/hunk extraction, manifest construction for changed_functions and changed_files scopes, empty-index edge case, bad-scope rejection. The hunk extraction test stages two files and edits a third worktree-only to confirm --cached doesn't bleed. - 5 new cmd pkg tests: validateModeFlags coverage for the four new conflict combos, selectMode confirms the Staged decision flag. - New diff_shared_test.go covers diffOpts.validate / isSet for the staged path. Closes knostic#18.
| if !quiet { | ||
| output.PrintKeyValue("Diff base", fmt.Sprintf("%s (%s)", m.BaseRef, shortSHA(m.BaseSHA))) | ||
| output.PrintKeyValue("Diff head", shortSHA(m.HeadSHA)) | ||
| if m.BaseRef == git.StagedRef { |
There was a problem hiding this comment.
🟡 Medium — UX · Python report consumers don't mirror this STAGED special-case
Issue: Nice that the CLI output renders Diff head: index (staged) for staged manifests. But the same head_sha = 0000…0000 placeholder flows into libs/openant-core/generate_report.py:109-121 (_build_header_subtitle) and libs/openant-core/export_csv.py:54-65 (_format_diff_banner), both of which call _short_sha(head_sha) → 00000000. End-users will see Incremental {repo} · 5e1d2a4..00000000 · … in the HTML report and CSV banner.
Suggestion: Mirror this special-case on the Python side — a one-line guard if base_ref == "STAGED": head = "index" in both _build_header_subtitle and _format_diff_banner keeps the report symmetric with the CLI output.
| if decision.Kind == config.ScanKindDiff { | ||
| manifestOpts.base = decision.Base | ||
| manifestOpts.scope = decision.Scope | ||
| manifestOpts.staged = decision.Staged |
There was a problem hiding this comment.
🟡 Medium — Architecture · Staged is consumed here but never persisted to ScanMeta
Issue: decision.Staged flows into manifestOpts.staged for this run, but the meta written a few lines down at scan.go:300-311 only persists Kind=diff, Base="", Scope=... — the staged marker is lost. Two consequences:
- Crashed-resume silently degrades to full. If a staged scan writes
meta.status=runningand then crashes, a subsequentopenant scan(no flags) hits the resume path atscan.go:272-276, which returnsmodeDecision{Kind: "diff", Base: "", Scope: ...}(Stageddefaults tofalse). InrunScan,prepareDiffManifestthen sees an unsetdiffOpts,isSet()returns false, and the diff manifest is silently skipped — falling back to a full scan without telling the user. - Audit trail is wrong. Terminal meta after a successful staged scan is indistinguishable from a malformed
--diff-baserun with empty base.
Suggestion: Either (a) add a Staged bool field (with a staged json tag, omitempty) to ScanMeta and propagate through both the writer and the resume path, or (b) skip persisting meta for staged runs entirely (since staged is meant to be ephemeral). (b) is simpler and matches the "pre-commit ephemeral" framing in the description.
What
Adds a
--stagedflag toopenant scanandopenant diffthat runs theincremental pipeline against the staged index (
git diff --cached) insteadof a committed ref. This is the workflow the DiffGuard project from #18
is built around: scan what is about to be committed, before the commit lands.
The existing
--diff-base/--pr/--incrementalmodes already coverpost-commit and PR-time scanning.
--stagedfills the pre-commit gap, whichmakes the diff-mode pipeline usable from a
pre-commithook or a quick local"scan what I'm about to commit" run without first committing.
Usage
In a
.git/hooks/pre-commit:How
BuildStagedManifest,StagedChangedFiles,StagedHunksForFileininternal/git/diff.go. SameManifestshape as the committed-diff path,with
base_ref="STAGED",base_sha=HEAD, and a zero placeholder forhead_sha(the index has no SHA).base=STAGEDcleanly without any change inlibs/openant-core.diffOptsgrows astaged bool;validate()makes--stagedmutuallyexclusive with
--diff-base/--pr.modeOpts/modeDecisionthread the staged choice throughselectMode,so
scan,diff, and the projectmeta.jsonrunning state agree on thescan kind.
four scopes (
--diff-base,--pr,--staged,--incremental).Notes
Untracked files are not in the index, so they do not appear in the staged
manifest. Pre-commit hooks typically
git addfirst; not second-guessingthat contract here.
git diff --cached --unified=0is the same hunk format the existingcommitted-diff path consumes, so the rest of the pipeline (parser tagging,
diff_filter, reporter) needs no changes.Tests
10 new tests, all passing:
internal/git:TestStagedChangedFilesAndHunks(stages two files plus aworktree-only edit, asserts
--cacheddoes not bleed),TestBuildStagedManifest,TestBuildStagedManifestChangedFilesScopeOmitsHunks,TestBuildStagedManifestEmptyIndex,TestBuildStagedManifestRejectsBadScope.cmd:TestValidateModeFlagsgains five rows for the new conflict combos(
staged + diffBase,staged + pr,staged + incremental,full + staged,staged alone);TestSelectModeStagedSetsStagedFlag.cmd/diff_shared_test.go:TestDiffOptsValidateStaged(7 sub-cases) andTestDiffOptsIsSetStaged.go vet ./...clean.go test ./...green on all packages.Closes #18.