Files
orca/src/main/git/source-control/status-read.ts
T
Neil 104f44222e perf(git): read the porcelain worktree mode instead of probing conflicted paths
Every porcelain-v2 `u` record already carries `mW`, the working-tree mode Git
stat'ed for that row: `000000` means the conflicted path is absent. Reading it
replaces the per-conflict `fs.access`, so the bounded-concurrency resolver, its
`= 8` cap, and the order/error-precedence invariant are unnecessary rather than
cheaper. `access()` stays only as a fallback for a malformed `mW`, so
`parseUnmergedEntry` keeps its signature and neither status-read.ts nor the
relay loop changes.

Also corrects two fixtures that encoded `mW=100644` for a file that does not
exist, which real Git never emits.
2026-09-03 20:40:04 -07:00

277 lines
11 KiB
TypeScript

import type {
GitStatusEntry,
GitStatusResult,
GitUpstreamStatus
} from '../../../shared/git-status-types'
import { StatusPorcelainParser } from '../../../shared/git-status-porcelain-parser'
import { resolveGitStatusLimit } from '../../../shared/git-status-limit'
import { stableInFlightKey } from '../../../shared/in-flight-promise-dedupe'
import type { GitBranchLineTotal } from '../../../shared/git-branch-line-total'
import {
beginGitStatusLineStatsCacheWrite,
clearGitStatusLineStatsCacheKey,
reuseOrRecomputeGitStatusLineStats
} from '../../../shared/git-status-line-stats-cache'
import { resolveWorktreeHostPath } from '../../../shared/git-metadata-path'
import { gitOptionalLocksDisabledEnv, gitStreamStdout } from '../runner'
import { findExistingWorktreeSymlinkPaths } from '../worktree-symlink-detection'
import type { GetStatusOptions } from './get-status-options'
import { statusReadLeaseOwner } from './git-read-cache-invalidation'
import { detectConflictOperation } from './git-conflict-operation'
import { parseUnmergedEntry } from '../../../shared/git-status-conflict-entries'
import { getEffectiveUpstreamStatusCacheKey } from './effective-upstream-status-cache'
import {
getShortBranchName,
readOrProbeEffectiveUpstreamStatus,
shouldProbeEffectiveUpstreamStatus
} from './effective-upstream-status-probe'
import { attachLineStats } from './status-line-stats'
import {
createBranchLineTotalInput,
getStatusLineStatsCacheKey
} from './status-branch-line-total-input'
/**
* Parse `git status --porcelain=v2` output into structured entries.
*/
export async function getStatus(
worktreePath: string,
options: GetStatusOptions = {}
): Promise<GitStatusResult> {
// Why nothing is cleared here: a status poll is a read. Dropping the in-flight diff entry
// mid-read only made a concurrent identical request start duplicate git work, and the
// settled diff cache is keyed on stamped git state, which a read cannot change anyway.
// Mutations invalidate both, through invalidateGitReadCaches.
const cacheKey = getStatusReadKey(worktreePath, options)
return statusReadLeaseOwner.lease(cacheKey, options.signal, (sharedSignal) =>
runGetStatus(worktreePath, { ...options, signal: sharedSignal })
)
}
function getStatusReadKey(worktreePath: string, options: GetStatusOptions): string {
// Why: each key part can change the output shape or runtime routing.
const limit = resolveGitStatusLimit(options.limit)
return stableInFlightKey([
worktreePath,
options.wslDistro ?? '',
options.admissionTier ?? 'status',
options.includeIgnored === true,
options.includeLineStats !== false,
options.reuseLineStats === true,
// Why: the result carries a total only for callers who asked, and only for
// this fork point, so a shared lease must never serve one to the other.
options.branchLineTotalMergeBase ?? '',
options.bypassEffectiveUpstreamNegativeCache === true,
limit,
// Why: this changes which entries survive, so it must not share a cache slot.
options.sharedLinkPaths ?? []
])
}
/** Remove untracked entries that are shared symlinks Orca created.
*
* Why this can't be left to Git: a directory-only ignore rule (`node_modules/`)
* matches the primary checkout's real directory but never the worktree's
* symlink, so Git reports it untracked forever — a phantom row in the diff and
* a permanently "dirty" worktree.
*
* Tight on both axes: an entry must be configured as shared *and* actually be a
* symlink. A regular file the user created at a configured name still shows up,
* and so does a symlink at a path nobody declared shared. Mutates `entries`. */
async function dropSharedSymlinkUntrackedEntries(
worktreePath: string,
entries: GitStatusEntry[],
options: GetStatusOptions
): Promise<void> {
const sharedLinkPaths = options.sharedLinkPaths ?? []
// Why: a clean tree has no untracked entries, so this costs nothing on the
// common status-poll path — no syscall, no config read, no subprocess.
if (sharedLinkPaths.length === 0 || !entries.some((entry) => entry.area === 'untracked')) {
return
}
const sharedLinks = new Set(
await findExistingWorktreeSymlinkPaths(worktreePath, sharedLinkPaths, {
wslDistro: options.wslDistro
})
)
if (sharedLinks.size === 0) {
return
}
for (let index = entries.length - 1; index >= 0; index--) {
const entry = entries[index]
if (entry.area === 'untracked' && sharedLinks.has(entry.path)) {
entries.splice(index, 1)
}
}
}
async function runGetStatus(
worktreePath: string,
options: GetStatusOptions = {}
): Promise<GitStatusResult> {
const lineStatsCacheKey = getStatusLineStatsCacheKey(worktreePath, options)
const lineStatsWriteToken =
options.includeLineStats === false ? null : beginGitStatusLineStatsCacheWrite(lineStatsCacheKey)
let effectiveUpstreamStatus: GitUpstreamStatus | undefined
let statusSucceeded = false
// Why: a bad limit (negative/fractional/NaN) breaks early-stop; require a valid non-negative int (0 disables the cap).
const limit = resolveGitStatusLimit(options.limit)
// Why: detectConflictOperation and git status are independent, so run them concurrently to save I/O latency.
const conflictPromise = detectConflictOperation(worktreePath, options)
// Why: core.quotePath=false keeps non-ASCII paths as raw UTF-8, not octal escapes, so entry.path is readable and lookups match.
const statusArgs = [
'-c',
'core.quotePath=false',
'status',
'--porcelain=v2',
'--branch',
'--untracked-files=all'
]
if (options.includeIgnored) {
statusArgs.push('--ignored=matching')
}
// Why: stream + parse and stop at `limit` so a huge un-ignored folder can't buffer enough to crash the process.
const parser = new StatusPorcelainParser()
let didHitLimit = false
// Why: attach rejection ownership before awaiting marker I/O, so a fast Git failure cannot become unhandled.
const statusSettlementPromise = Promise.allSettled([
(async () => {
const result = await gitStreamStdout(statusArgs, {
cwd: worktreePath,
wslDistro: options.wslDistro,
admissionTier: options.admissionTier,
preferWslDirectGit: true,
// Why: status polling is read-like; disable optional locks to avoid racing terminal Git on index.lock.
env: gitOptionalLocksDisabledEnv(),
signal: options.signal,
onStdout: (chunk) => parser.update(chunk, limit)
})
if (!result.stoppedEarly) {
parser.finish()
}
return result
})()
])
const conflictOperation = await conflictPromise
try {
const [statusResult] = await statusSettlementPromise
if (statusResult.status === 'rejected') {
throw statusResult.reason
}
didHitLimit = statusResult.value.stoppedEarly
statusSucceeded = true
} catch (error) {
// Why: an aborted scan must reject, not resolve as an empty result.
if (options.signal?.aborted) {
throw error
}
// Not a git repo or git not available
}
const entries: GitStatusEntry[] = []
const { head, branch, upstreamName, upstreamAheadBehind } = parser.branch
// Why: git runs in the distro and answers in its namespace; the working-tree probes below run here.
const hostWorktreePath = resolveWorktreeHostPath(worktreePath, options) ?? worktreePath
// Why: resolve deferred conflicts in Git's output order so the cap cannot hide
// an early conflict behind ordinary rows that appeared later in the stream.
for (const record of parser.statusRecords) {
if (didHitLimit && entries.length >= limit) {
break
}
if (record.type === 'entry') {
entries.push(record.entry)
} else {
const unmergedEntry = await parseUnmergedEntry(hostWorktreePath, record.line)
if (unmergedEntry) {
entries.push(unmergedEntry)
}
}
}
await dropSharedSymlinkUntrackedEntries(worktreePath, entries, options)
if (statusSucceeded && !didHitLimit && shouldProbeEffectiveUpstreamStatus(branch, upstreamName)) {
const branchName = getShortBranchName(branch)
if (branchName) {
const cacheKey = getEffectiveUpstreamStatusCacheKey(
worktreePath,
branchName,
upstreamName,
options
)
try {
// Why: the shared probe/caches serve concurrent reads, so run it unbound from this signal — one abort mustn't reject it for others.
const { signal: _requestSignal, ...sharedProbeOptions } = options
effectiveUpstreamStatus = await readOrProbeEffectiveUpstreamStatus(
cacheKey,
worktreePath,
branchName,
sharedProbeOptions,
options.bypassEffectiveUpstreamNegativeCache === true
)
} catch {
// Why: don't fail status polling on a transient upstream-probe error; the explicit upstream path surfaces those.
}
}
}
// Why: line counts run only for areas with entries (clean tree = 0 calls); skip past the limit to avoid numstat over a huge set.
let branchLineTotal: GitBranchLineTotal | undefined
if (!didHitLimit && lineStatsWriteToken !== null) {
const branchLineTotalInput = createBranchLineTotalInput(
worktreePath,
entries,
options,
statusSucceeded
)
const lineStats = await reuseOrRecomputeGitStatusLineStats({
cacheKey: lineStatsCacheKey,
head,
entries,
writeToken: lineStatsWriteToken,
reuse: options.reuseLineStats === true,
isAborted: () => options.signal?.aborted === true,
recompute: () => attachLineStats(worktreePath, entries, options),
...(branchLineTotalInput ? { branchLineTotal: branchLineTotalInput } : {})
})
branchLineTotal = lineStats.branchLineTotal
} else if (lineStatsWriteToken !== null) {
clearGitStatusLineStatsCacheKey(lineStatsCacheKey, lineStatsWriteToken)
}
// Why: an abort after the stream (unmerged/upstream/line-stats work) must still reject, not resolve.
if (options.signal?.aborted) {
const error = new Error('The operation was aborted.')
error.name = 'AbortError'
throw error
}
return {
entries,
conflictOperation,
head,
branch,
...(options.includeIgnored ? { ignoredPaths: parser.ignoredPaths } : {}),
...(branchLineTotal ? { branchLineTotal } : {}),
...(didHitLimit ? { didHitLimit: true, statusLength: parser.statusLength } : {}),
...(statusSucceeded
? {
upstreamStatus:
effectiveUpstreamStatus ??
(upstreamName
? {
hasUpstream: true,
upstreamName,
ahead: upstreamAheadBehind?.ahead ?? 0,
behind: upstreamAheadBehind?.behind ?? 0
}
: { hasUpstream: false, ahead: 0, behind: 0 })
}
: {})
}
}