Expand description
Git diff scanner - on-demand git subprocess invocations that
produce a GitDiffSnapshot describing the project’s current
changeset.
Polled (not watched): the workspace reaches scan as
forge_workspace::env::git_diff::scan, a wildcard re-export of this
module through the workspace facade, and runs it on the
“snapshot is None OR age ≥ 10 s → fetch” rule while a seat is
held. The snapshot carries branch info alongside diff stats, so this
module replaces both the previous file-watcher GitContextWatcher
and the per-turn refresh hooks that fed it.
The snapshot exposes two independent diff layers so callers can
render both simultaneously when both apply (worker on a topic
branch with uncommitted edits, for instance). Each layer is a
LayerState with three variants:
worktree: uncommitted edits vs HEAD (the dirty tree).Cleanwhen the tree is clean.branch_ahead: the branch’s commits ahead of the default branch.Cleanon the default branch, on detached HEAD, or when there’s no resolvable default.ScanFailedcarries the per-layer subprocess-error signal.
scan always returns a value. Subprocess failures, missing
repos, oversize output, and timeouts all collapse to a non-InRepo
repo_gate (NotARepo / ScannerFailed); the failure surfaces in
the trace log so a real issue can be diagnosed without breaking the
rendering path.
Re-exports§
use std::collections::HashMap;🔒use std::path::Path;🔒use std::path::PathBuf;🔒use std::sync::OnceLock;🔒use std::time::Duration;🔒use std::time::Instant;🔒use parking_lot::Mutex;🔒use super::git_command;🔒use forge_primitives::git::FileStatus;🔒use forge_primitives::git::GitBranch;🔒use forge_primitives::git::GitIssueRef;🔒use forge_primitives::git::GitPrInfo;🔒use forge_primitives::git_diff::BaseState;🔒use forge_primitives::git_diff::GitBranchAhead;🔒use forge_primitives::git_diff::GitDiffFile;🔒use forge_primitives::git_diff::GitDiffSnapshot;🔒use forge_primitives::git_diff::GitDiffStats;🔒use forge_primitives::git_diff::LayerState;🔒use forge_primitives::git_diff::PrRefresh;🔒use forge_primitives::git_diff::RepoGate;🔒use serde::Deserialize;🔒use serde::Deserialize;🔒use tokio::time::timeout;🔒
Modules§
- content
- Bounded content read beside the stats scan: what a seat’s changed files say, as raw unified-diff hunks.
- file
- One file’s whole content at a revision, as its lines.
- hunks
- Per-file hunk scanner for the
/diffoverlay. - resolver
- Re-anchoring resolver for durable review threads.
Structs§
- Base
Measurement - One completed base refresh: when it measured, what it found, and git’s own words when it could not run.
- Base
Refresh - What one base refresh answered.
- BaseRun 🔒
- One base refresh in flight, and the answer it will publish.
- Fetched 🔒
- What one fetch’s child answered.
- GhApi
Pull 🔒 commits/<sha>/pullsREST entry shape. Only the fields the selection rule reads are deserialised;gh apiadds others (title,body, …) that serde silently drops.- GhIssue
Entry 🔒 closingIssuesReferenceselement shape. Same selective-field pattern asGhPrEntry.- GhPr
View 🔒 gh pr view --jsonwrapper shape.- Leader
Guard 🔒 - Publish the leader’s answer, or a failure if the leader ended without one: a caller waiting on a run whose leader panicked or was cancelled must not wait for the process’s life.
Enums§
- GhNot
Found 🔒 - How a
ghcall’s “not found” failure should be classified.Toleratecovers the shape the endpoint legitimately produces for a mid-work state (an unpushed HEAD), which is a completed query with a definitive negative rather than a failure. - GitOutput 🔒
- Result of one
gitsubprocess invocation. Callers treat all failure variants the same way (collapse toNoRepo/ zero stats), but the variants are split so the log captures the right context -Failedmeans non-zero exit with stderr that an operator might need;Emptyis a legitimate “ran fine, no output” signal (the common case forstatus --porcelainon a clean tree). - Numstat
Error - Underlying
git diff --numstatsubprocess didn’t return a usable result. Callers map this toLayerState::ScanFailedso the renderer can surface the failure. The variants carry enough classification to triage from the log emitted byrun_git; downstream callers don’t differentiate them today but the type keeps the option open. - Origin
Remote 🔒 - Whether the checkout has an
originremote: yes, no, or a probe that could not answer. - PrLookup 🔒
- What one PR lookup produced.
Failedis kept apart fromNoneso a transientghblip cannot evict a known-good cached PR. - Repo
Presence 🔒 - Whether a git repository is reachable from a scan cwd, and whether git could answer the question at all. The third variant matters: “git told me there is no repo” and “git would not run” must not collapse together, or a missing git binary reads as a non-repo and silently suppresses the GIT section instead of reporting itself.
Constants§
- COMMAND_
TIMEOUT 🔒 - Per-subprocess timeout. A scan’s worst case is one budget per child, and a
repository with no remote and no local default branch issues ten of them
(
SCAN_CHILDREN). In practice every command returns in <100 ms on a local repo, and timeouts only fire on hung network mounts or genuinely huge repositories. - COMMITS_
WALK_ 🔒CAP - How many commits the chain walk reads. Past it the count above the list still tells the truth; the list is what a reader scans, not what they count.
- DISPLAY_
FETCH_ 🔒BUDGET - The budget a drawing read’s background refresh gets.
- FAILURE_
WORDS_ 🔒CAP - Carried characters of git’s failure line. A page draws it in one sentence
beside the base note; git’s own
fatal:lines are far shorter than this, and the cap applies to the redacted line. - PR_
REFETCH_ 🔒INTERVAL - Minimum gap between PR-lookup attempts for one branch+sha, even
when the attempt found nothing or failed - without this a cached
Nonewould stick for the branch’s whole life (a PR opened after the session’s first scan would never show) and a persistentghfailure would hammer every 10 s scan. - PUSHED_
SHA_ 🔒WALK_ CAP - Max commits the pushed-sha walk examines. Bounds the pathological case (a deep unpushed history) so one scan can’t walk the whole repo; 500 first-parent commits is generous headroom over any real worktree’s unpushed depth.
- SCAN_
CHILDREN 🔒 - The children one scan issues for a repository with no remote and no local
default branch:
rev-parse, theorigin/HEADprobe plus its four candidates (all missed),status, the worktree layer’snumstatandname-status, andfor-each-reffor the pushed sha. The PR lookup adds aghchild only once a pushed sha exists, which such a repository has none of. - SCAN_
WORST_ CASE - One scan’s worst case for the shape
SCAN_CHILDRENcounts, which is what a test waiting on a scan derives its own bound from. Test-only: production reads the per-child budget, never the count. - STDERR_
LOG_ 🔒CAP - Cap on captured stderr surfaced into the failure log. Far below the
stdout cap because stderr is conversational - a couple of
fatal:lines is more than enough context. - STDOUT_
SIZE_ 🔒CAP - Max bytes accepted from any single
gitsubprocess invocation.--numstatis small in practice; the per-filegit diff <target> --no-ext-diff -- <path>calls produced byhunks::scanare each bounded by one file’s worth of changes. 8 MiB is generous headroom for genuinely huge single-file diffs (lockfiles, generated source, vendored snapshots) without exposing the scan to unbounded memory growth on a corrupted-git-output edge case. - TOP_
FILE_ 🔒COUNT - Number of file rows surfaced in the rendered diff list. The renderer collapses the rest into a “+N more” line; the count above the list tells the truth either way.
- UNBOUNDED 🔒
- The budget a command handed no deadline reads as: no cap at all. Every refresh runs under a budget of its own (a kick’s, or a sessionless read’s), so this is what “no deadline” means to the helpers rather than a mode any read takes.
- WATCH_
REFRESH_ 🔒INTERVAL - The kick’s own cadence. The chain stays honest while no review page is open because a scan freshens the ref for the next one; throttled so a burst of scans fetches at most once a window.
Statics§
- BASE_
RUNS 🔒 - One base refresh in flight per repository and base branch, shared by every kick that arrives while it runs: two checkouts of one repository fetch the same ref once. Weak, so a repository nobody is refreshing for leaves nothing behind; dead entries are pruned on the next look.
- LAST_
MEASURED 🔒 - What each checkout’s last completed refresh measured, so a scan can carry it onto the seat’s row without waiting for the fetch itself. Pruned on the same window as the kick, and the row keeps its stored base when an entry goes.
- LAST_
WATCH_ 🔒REFRESH - Per-checkout last refresh, keyed by the scan cwd.
Functions§
- answer_
measurement 🔒 - The answer a stored check gives a drawing read: its state, words and instant, pinned to the local tip the caller read beside it.
- changed_
file_ count - How many files
cwd’s worktree has changed against HEAD: tracked modifications plus untracked files, one--porcelainline each.Errcarries the repo gate, so a caller can tell “not a repo” from a git that would not run rather than reading either as a clean tree. - checked_
out_ 🔒on_ default - Whether
branchis the default a scan measures against, compared as plain names:default_branchmay be a remote-tracking ref (origin/main), and a checked-outmainis the default rather than a feature branch ahead of it. - child_
output 🔒 - One child’s output under
budget;Nonewhen the budget ran out first and the child was killed. - claim_
refresh_ 🔒slot - Claim a kick’s slot for
key, stampingnowwhen it is due, so two scans cannot both fetch. - classify_
git 🔒 - One git child’s output, classified, with its failures logged.
- classify_
rev_ 🔒parse - Classify
git rev-parse --abbrev-ref HEADoutput into either the branch string to proceed with, or the terminalrepo_gate. Empty stdout is a clean “not a repo” signal (git ran and reported it); Oversize means the scan subprocess itself misbehaved. - clear_
git_ point_ for_ tests - Drop
cwd’s pointer, back to the machine’sgit. - clipped 🔒
textcut tocapbytes without splitting a character: git’s stderr is arbitrary bytes, and a slice at a byte offset inside one panics.- commit_
count_ 🔒in_ range - Count commits in
<base>..HEADviagit rev-list --count. ReturnsNoneon any failure path (subprocess error, parse failure, anomalous empty output) so callers can route the signal intoLayerState::ScanFailedinstead of masking the failure as 0. Each non-Ok branch emits a WARN log mirroring thegh_pr_lookup_*pattern. - commits_
in_ 🔒range - The branch’s commit chain ahead of
base, newest first, capped atCOMMITS_WALK_CAP: the count says how many there are, these say what they are, and each carries the files it changed. Two walks of the same range ---numstatfor the counts,--name-statusfor the marks, which git will not emit together.%his the short sha,%sthe subject; the unit separator cannot occur in either, so one line splits cleanly. - current_
branch - Run the full scan sequence against
cwdand return a snapshot. Always succeeds - every failure path collapses to a non-InReporepo_gateand a log line naming the step that failed. Callers should treat the snapshot as authoritative for rendering regardless of which variant came back. - dot_
git_ 🔒present - Whether
.gitis on disk, without following symlinks - a dangling.gitsymlink is a broken checkout rather than an absent repo, andPath::try_existsreports it as missing because it stats the target. Anything but a definite “not found” counts as present, so an unreadable parent biases toRepoPresence::Unusableas well. - fetch_
base 🔒 - Fetch one remote-tracking branch without touching the rest of the user’s
checkout: an explicit refspec with its destination, so a remote whose own
fetch refspec differs still updates the ref this layer reads;
--no-auto-gcso a killed child leaves no maintenance daemon behind;--no-write-fetch-headso theirFETCH_HEADis not clobbered on every open; and--quietso progress never rides a captured stream. - fetch_
closing_ 🔒issues - Fetch the PR’s closing-issue list via
gh pr view <n> --json closingIssuesReferences. The RESTcommits/<sha>/pullsresponse doesn’t carry closing issues (a GraphQL-only field), so this second call keeps the→ closes #Mtail rendering. Degrades to an empty list on any failure - the PR row is the headline, the closes are the tail. - fetch_
pr_ 🔒for_ pushed_ sha - Look up the open PR containing
pushed_sha. ReturnsPrLookup::Nonewhen the query ran and answered “no open PR” (nothing pushed, or no open PR contains the sha) andPrLookup::Failedon every failure path -ghmissing, unauthenticated, not a github repo, JSON parse error. Failures carry a structured event so operators can grep forgh_pr_lookup_*when triaging “PR row never shows”; the subprocess shapes are at DEBUG and the parse failure at WARN. The commit-not-on-remote shape logs at DEBUG instead because an unpushed HEAD is a normal mid-work state, not an operator signal. - first_
line 🔒 - Git’s own words for a failed child: the first non-empty stderr line, uncapped so the redactor can see the whole URL before anything is cut.
- give_
back_ 🔒slot - A run that answered no check gives the slot back. A cut or aborted run measured nothing; leaving its claim stamped would answer silence for the whole window on the one seat whose read is what heals it.
- is_
lock_ 🔒failure - Whether git refused the fetch because a ref lock it needed is held - by another git in the same checkout, or by a killed child that left one.
- is_
worktree_ 🔒dirty - Returns
Some(dirty)whengit statusran cleanly (Ok/Empty);Nonewhen the subprocess hitFailed/Oversize. Callers thread theNonesignal intoLayerState::ScanFailedso the renderer can surface “(scan failed)” for layer 1 instead of silently rendering a clean tree when the probe upstream of numstat collapsed. - join_
base_ 🔒run - The run for
key: the one already in flight (or its answer, when it has finished and a caller still holds it), or a new one the caller leads. - kick_
background_ 🔒fetch - Kick a throttled, non-blocking base refresh so the remote-tracking ref the
branch-ahead layer compares against is fresher for the NEXT scan. Only fires
for a remote-tracking default (
origin/...); a purely-local default has no remote to fetch. Failures log fromfetch_base, which owns the one fetch path with its flags and lock line. - last_
base_ measurement - This checkout’s last completed refresh of
default_branch.Noneuntil one has completed; a reader appliesmeasurement_in_windowitself, since the store only prunes on write. - lead_
base_ 🔒run - local_
base 🔒 - The state of a base that names no remote-tracking ref, and the sha it resolves to: the probes a local base owes.
- local_
base_ 🔒tracks_ upstream - Whether the local base itself tracks a remote (
main@{upstream}): a base that does can be behind that remote exactly as anorigin/...can, and nothing was fetched for it here.Nonewhen git could not answer, which the caller treats as unverified rather than as “nothing to lag behind”. - local_
tip 🔒 - The sha
referenceresolves to, orNonewhen git cannot read it. - measurement_
in_ 🔒window - Whether a measurement still answers a drawing read: the store’s own window, which is how long the store keeps one.
- name_
statuses 🔒 - One changed path’s status against
target, fromgit diff --name-status- the same classifier the hunks pass reads, so the glyph beside a file matches the one its diff body would draw. Best-effort: a failed read leaves the counts alone. - numstat 🔒
- Run
git <args>and parse the--numstatoutput. Returns the top-TOP_FILE_COUNTfiles (sorted by total changes desc, alpha tie-break) plus the full file count and overall add/remove totals. ReturnsErr(NumstatError)when the underlyinggitsubprocess hitFailed/Oversize; callers map that signal toLayerState::ScanFailedso the renderer can distinguish “scan failed” from “clean tree, nothing to show”.Empty(exit 0, no stdout) collapses to a zero-row stats block since that’s the legitimate “no diff to report” outcome. - origin_
remote 🔒 - Ask whether
cwdhas anoriginremote at all: a local base beside one is a base nobody fetched, which is not the same as a repository without a remote to lag behind. - parse_
commit_ 🔒marks - Marks per commit from a
--name-statuswalk of the same chain: the status lines carry no sha of their own, so each block belongs to the format line that opened it. - parse_
numstat 🔒 - Parse
<added>\t<removed>\t<path>lines. Skips binary entries (addedorremovedreported as-). Usessplitn(3, '\t')so paths containing tabs survive intact. - parse_
numstat_ 🔒line - One
<added>\t<removed>\t<path>line, orNonefor anything else - a binary entry (counts as-) and a format line among them. - pick_
open_ 🔒pr - Select the open PR from the
commits/<sha>/pullsREST response. Multiple open PRs can contain the same commit (stacked PRs, cherry-picks); the most recentlyupdated_atone wins - the actively-evolving PR is the right answer in stacked workflows - with the higher number breaking ties. One row renders, never a list. - point_
git_ at_ for_ tests - Point
cwd’sgitspawns atprogram: thetestingfeature’s seam, so a crate whose fixtures need a held child drives the same wrapper this crate’s own tests do. Pair withclear_git_point_for_tests. - pr_
cache_ 🔒fresh - Whether the previous snapshot’s PR block is still reusable: the
branch name and pushed sha both match the current scan AND the
last lookup attempt is less than
PR_REFETCH_INTERVALold. The sha leg - not the branch name - is what makes a PR opened or pushed after an earlier scan show up; the timer leg unsticks a cachedNone(a scan that ran before the PR existed would otherwise holdpr: Nonefor the branch’s whole life). - pr_
for_ 🔒head_ within - Resolve PR info for the branch from
prev’s cache when fresh, else query fresh by the pushed sha. Stampspr_fetched_aton the returned attempt time so failed / empty lookups rate-limit too. AFailedlookup keepsprev’s PR when branch and sha still match - a gh blip must not flash the row off. - pr_
refresh_ due - Whether a scan’s PR block is past its refetch window, so a scan that may reach the network would look it up again.
- redact 🔒
- Git’s words with any credential a remote URL carries taken out.
- refresh_
base 🔒 - The one refresh path: one run per repository and base branch, joined by a kick that arrives while it works, and its answer stored as the checkout’s measurement for the reads that follow.
- refresh_
base_ for_ display - Answer the base a drawn branch diff is measured against, and keep it fresh off the read.
- refresh_
base_ now - Answer the base by running the refresh on this call: the one path that waits, for a seat with no session.
- refresh_
base_ 🔒owned - One refresh’s own work: the fetch, then the state read off the ref that
fetch wrote, every step sharing
budget. - refresh_
due 🔒 - Whether a watch refresh is due: none yet, or the last one is
windowold. - refresh_
once 🔒 - Join (or lead) one run for
keyand answer it. A kick that arrives while another works joins it rather than fetching the same base twice. - remaining 🔒
- The budget left before
deadline, zero once spent andUNBOUNDEDwhen there is no deadline at all. - remember_
measurement 🔒 - rename_
target 🔒 - A
--numstatpath resolved to the file’s new name: a rename crosses asdir/{old => new}.rs, orold => newwhen the two share no affixes, while--name-statusand the diff body both speak the new path alone - so the merge inscanneeds the same key from every read. - repo_
key 🔒 - The repository a checkout belongs to, as the fetch gate’s key: git’s common dir, canonicalised.
- repo_
presence 🔒 - Ask whether
cwdsits inside a git repo withrev-parse --git-dir, whose exit status answers repo-existence alone: zero inside any work tree including one with no commits, non-zero outside one and for a path that does not exist. Matching git’sfatal:prose would not work here even setting version drift aside - a deleted.git/HEADand a plain non-repo emit the same line. - repo_
presence_ 🔒within repo_presencewith its command budget spelled; seescan_within.- resolve_
default_ 🔒branch - Resolve the ref a feature branch’s “ahead” diff compares against.
Prefers the remote-tracking
origin/<default>(viaorigin/HEAD, elseorigin/main/origin/master) so a locally-stalemaincan’t fold already-merged commits into the diff; falls back to a purely-localmain/masterfor a repo with nooriginremote. The returned value IS the compare-ref - theorigin/prefix carries the remote-vs-local decision, so callers wanting the plain branch name strip it.Nonewhen nothing resolves. - resolve_
pushed_ 🔒sha - Resolve HEAD’s newest pushed ancestor: walk HEAD’s first-parent
history newest-first (bounded by
cap) and stop at the first sha the remote-tracking refs already contain.Nonewhen nothing of HEAD’s ancestry is pushed or no remote refs exist (a truthful negative); a git failure also yieldsNonebut is an UNRESOLVABLE lookup, not a negative - the row may flicker off for one scan while git is broken and recovers on the next scan. The walk happens inside git so a worktree ahead of the PR tip by unpushed commits still resolves the commit the PR was pushed from. - rev_
parse_ 🔒gate - Resolve the rev-parse gate for
cwd: the raw branch string, or the terminalrepo_gate. The repo-existence probe runs only on a non-zero exit, so the healthy path still costs a single subprocess. - run_gh 🔒
- Spawn
gh <args>fromcwd(gh derives the github repo from the current working directory - there’s no-Cequivalent), carryingenvas the project’s own identity. Mirrorsrun_git’s timeout / classification / DEBUG logging so failures distinguish “gh: command not found” (binary missing) from “gh: To use GitHub CLI in a Git repository, please run …” (not a github remote) from “no pull requests found” (legitimate empty result) when triaging “PR row never shows”. - run_git 🔒
- Spawn
git <args>againstcwd, await with a per-command timeout, return classified output. Failures log DEBUG with the captured stderr, so triage of “the GIT section shows nothing” can still tell “clean tree” from “corrupt index / fatal: …” without re-running, at a level that does not report a miss the probe makes on every scan of a perfectly ordinary repo. - run_
git_ 🔒within run_gitwith its command budget spelled: the shell gives agitchildCOMMAND_TIMEOUT, and a test gives it one of its own.- same_
branch_ 🔒and_ sha - Whether
prevdescribes the same branch and pushed sha the current scan resolved - the condition under which a failed lookup may keepprev’s cached PR. - scan
enviscwd’s project’s own declared environment, carried as the identity of everyghcall the scan makes, so the PR row reads as the project rather than as the server’s ambient login.- scan_
pinned - A read pinned to one base revision: the branch-ahead layer is computed
against
base(a sha a refresh just resolved) rather than against the ref’s name, so this read and a content read taken beside it describe one commit even if the ref moves between them. Never fetches and never looks the PR up: the caller has already refreshed the base and is drawing it. - scan_
serving_ cached_ pr scanfor a read a client is waiting on: a PR cache past its window is served for the branch and sha it belongs to rather than looked up, so the read never waits on the network; a cache from another branch or push answers nothing. The seat’s own loop scans withscanand pushes the refreshed row.- scan_
within 🔒 scanwith the scan’s command budget spelled: the shell gives every child a scan spawns the 10 sCOMMAND_TIMEOUT, and a test gives them one of its own.- scan_
without_ base_ fetch scanfor a read that will fetch the base itself a moment later: the same local scan with none of the background kick, so one read of a sessionless seat pays one fetch - its own - and never queues that fetch behind forge’s on the shared per-repository gate.- ssh_
is_ 🔒forges - Whether forge builds the ssh command for this checkout’s network children:
true when the environment names none and the repository’s own
core.sshCommandis unset, becauseGIT_SSH_COMMANDoutranks both. - stamp_
refresh_ 🔒slot - Stamp
key’s slot whatever it held: a refresh that just ran left the ref fresh, so a kick inside the window has nothing to fetch. - stats_
of 🔒 - One file list as a stats block: the full totals, then the
top-
TOP_FILE_COUNTfiles by churn (alpha tie-break) - the count above the list tells the truth either way. The shape a layer’s stats and each commit’s own stats both take. - stored_
base 🔒 - Answer the last completed check the store holds, or the base unchecked, pinned to the local tip read beside it.
- track_
background_ 🔒fetch - Hand a kicked fetch’s handle to the test-only registry above. A production build discards it: the fetch is fire-and-forget, which is what the scan promises its caller.