Skip to main content

Module git_diff

Module git_diff 

Source
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). Clean when the tree is clean.
  • branch_ahead: the branch’s commits ahead of the default branch. Clean on the default branch, on detached HEAD, or when there’s no resolvable default. ScanFailed carries 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 /diff overlay.
resolver
Re-anchoring resolver for durable review threads.

Structs§

BaseMeasurement
One completed base refresh: when it measured, what it found, and git’s own words when it could not run.
BaseRefresh
What one base refresh answered.
BaseRun 🔒
One base refresh in flight, and the answer it will publish.
Fetched 🔒
What one fetch’s child answered.
GhApiPull 🔒
commits/<sha>/pulls REST entry shape. Only the fields the selection rule reads are deserialised; gh api adds others (title, body, …) that serde silently drops.
GhIssueEntry 🔒
closingIssuesReferences element shape. Same selective-field pattern as GhPrEntry.
GhPrView 🔒
gh pr view --json wrapper shape.
LeaderGuard 🔒
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§

GhNotFound 🔒
How a gh call’s “not found” failure should be classified. Tolerate covers 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 git subprocess invocation. Callers treat all failure variants the same way (collapse to NoRepo / zero stats), but the variants are split so the log captures the right context - Failed means non-zero exit with stderr that an operator might need; Empty is a legitimate “ran fine, no output” signal (the common case for status --porcelain on a clean tree).
NumstatError
Underlying git diff --numstat subprocess didn’t return a usable result. Callers map this to LayerState::ScanFailed so the renderer can surface the failure. The variants carry enough classification to triage from the log emitted by run_git; downstream callers don’t differentiate them today but the type keeps the option open.
OriginRemote 🔒
Whether the checkout has an origin remote: yes, no, or a probe that could not answer.
PrLookup 🔒
What one PR lookup produced. Failed is kept apart from None so a transient gh blip cannot evict a known-good cached PR.
RepoPresence 🔒
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 None would stick for the branch’s whole life (a PR opened after the session’s first scan would never show) and a persistent gh failure 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, the origin/HEAD probe plus its four candidates (all missed), status, the worktree layer’s numstat and name-status, and for-each-ref for the pushed sha. The PR lookup adds a gh child 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_CHILDREN counts, 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 git subprocess invocation. --numstat is small in practice; the per-file git diff <target> --no-ext-diff -- <path> calls produced by hunks::scan are 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 --porcelain line each. Err carries 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 branch is the default a scan measures against, compared as plain names: default_branch may be a remote-tracking ref (origin/main), and a checked-out main is the default rather than a feature branch ahead of it.
child_output 🔒
One child’s output under budget; None when the budget ran out first and the child was killed.
claim_refresh_slot 🔒
Claim a kick’s slot for key, stamping now when 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 HEAD output into either the branch string to proceed with, or the terminal repo_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’s git.
clipped 🔒
text cut to cap bytes 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>..HEAD via git rev-list --count. Returns None on any failure path (subprocess error, parse failure, anomalous empty output) so callers can route the signal into LayerState::ScanFailed instead of masking the failure as 0. Each non-Ok branch emits a WARN log mirroring the gh_pr_lookup_* pattern.
commits_in_range 🔒
The branch’s commit chain ahead of base, newest first, capped at COMMITS_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 - --numstat for the counts, --name-status for the marks, which git will not emit together. %h is the short sha, %s the subject; the unit separator cannot occur in either, so one line splits cleanly.
current_branch
Run the full scan sequence against cwd and return a snapshot. Always succeeds - every failure path collapses to a non-InRepo repo_gate and 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 .git is on disk, without following symlinks - a dangling .git symlink is a broken checkout rather than an absent repo, and Path::try_exists reports it as missing because it stats the target. Anything but a definite “not found” counts as present, so an unreadable parent biases to RepoPresence::Unusable as 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-gc so a killed child leaves no maintenance daemon behind; --no-write-fetch-head so their FETCH_HEAD is not clobbered on every open; and --quiet so progress never rides a captured stream.
fetch_closing_issues 🔒
Fetch the PR’s closing-issue list via gh pr view <n> --json closingIssuesReferences. The REST commits/<sha>/pulls response doesn’t carry closing issues (a GraphQL-only field), so this second call keeps the → closes #M tail 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. Returns PrLookup::None when the query ran and answered “no open PR” (nothing pushed, or no open PR contains the sha) and PrLookup::Failed on every failure path - gh missing, unauthenticated, not a github repo, JSON parse error. Failures carry a structured event so operators can grep for gh_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) when git status ran cleanly (Ok / Empty); None when the subprocess hit Failed / Oversize. Callers thread the None signal into LayerState::ScanFailed so 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 from fetch_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. None until one has completed; a reader applies measurement_in_window itself, 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 an origin/... can, and nothing was fetched for it here. None when git could not answer, which the caller treats as unverified rather than as “nothing to lag behind”.
local_tip 🔒
The sha reference resolves to, or None when 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, from git 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 --numstat output. Returns the top-TOP_FILE_COUNT files (sorted by total changes desc, alpha tie-break) plus the full file count and overall add/remove totals. Returns Err(NumstatError) when the underlying git subprocess hit Failed / Oversize; callers map that signal to LayerState::ScanFailed so 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 cwd has an origin remote 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-status walk 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 (added or removed reported as -). Uses splitn(3, '\t') so paths containing tabs survive intact.
parse_numstat_line 🔒
One <added>\t<removed>\t<path> line, or None for anything else - a binary entry (counts as -) and a format line among them.
pick_open_pr 🔒
Select the open PR from the commits/<sha>/pulls REST response. Multiple open PRs can contain the same commit (stacked PRs, cherry-picks); the most recently updated_at one 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’s git spawns at program: the testing feature’s seam, so a crate whose fixtures need a held child drives the same wrapper this crate’s own tests do. Pair with clear_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_INTERVAL old. 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 cached None (a scan that ran before the PR existed would otherwise hold pr: None for 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. Stamps pr_fetched_at on the returned attempt time so failed / empty lookups rate-limit too. A Failed lookup keeps prev’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 window old.
refresh_once 🔒
Join (or lead) one run for key and 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 and UNBOUNDED when there is no deadline at all.
remember_measurement 🔒
rename_target 🔒
A --numstat path resolved to the file’s new name: a rename crosses as dir/{old => new}.rs, or old => new when the two share no affixes, while --name-status and the diff body both speak the new path alone - so the merge in scan needs 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 cwd sits inside a git repo with rev-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’s fatal: prose would not work here even setting version drift aside - a deleted .git/HEAD and a plain non-repo emit the same line.
repo_presence_within 🔒
repo_presence with its command budget spelled; see scan_within.
resolve_default_branch 🔒
Resolve the ref a feature branch’s “ahead” diff compares against. Prefers the remote-tracking origin/<default> (via origin/HEAD, else origin/main / origin/master) so a locally-stale main can’t fold already-merged commits into the diff; falls back to a purely-local main / master for a repo with no origin remote. The returned value IS the compare-ref - the origin/ prefix carries the remote-vs-local decision, so callers wanting the plain branch name strip it. None when 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. None when nothing of HEAD’s ancestry is pushed or no remote refs exist (a truthful negative); a git failure also yields None but 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 terminal repo_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> from cwd (gh derives the github repo from the current working directory - there’s no -C equivalent), carrying env as the project’s own identity. Mirrors run_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> against cwd, 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_git with its command budget spelled: the shell gives a git child COMMAND_TIMEOUT, and a test gives it one of its own.
same_branch_and_sha 🔒
Whether prev describes the same branch and pushed sha the current scan resolved - the condition under which a failed lookup may keep prev’s cached PR.
scan
env is cwd’s project’s own declared environment, carried as the identity of every gh call 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
scan for 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 with scan and pushes the refreshed row.
scan_within 🔒
scan with the scan’s command budget spelled: the shell gives every child a scan spawns the 10 s COMMAND_TIMEOUT, and a test gives them one of its own.
scan_without_base_fetch
scan for 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.sshCommand is unset, because GIT_SSH_COMMAND outranks 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_COUNT files 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.

Type Aliases§

RunKey 🔒
A run’s address: the repository’s common dir and the base branch.
Runs 🔒
Every run in flight, weak so a repository nobody is refreshing for leaves nothing behind.