Files
orca/src/main/git/worktree-scan-cache.ts
T
Neil 8ac1c6e2ac perf(git): bound ref and worktree scans (#17655)
* perf(git): bound ref and worktree scans

* fix(repo-search): clamp oversized ref limits

* fix(worktree): keep strict worktree listing unshared

The shared-scan re-export flipped every `listWorktreesStrict` caller from an
isolated subprocess to the coalesced scan. `git worktree prune` in the removal
recovery path does not bump the scan generation, so a post-prune verification
could join a pre-prune scan, see the stale row, and report a successful removal
as a stale registration. The same gap defeats the post-archive-hook rechecks
that exist to catch an external Git client locking the row.

Restore the unshared export and make coalescing opt-in via
`listWorktreesSharedStrict`, which existing callers already use deliberately.

* fix(git): separate a proven absent ref from a failed probe

`show-ref --verify --quiet` exits 1 for a missing ref, but so does `wsl.exe`
when its own launch fails, so reading any exit 1 as absence collapsed
`unverifiable` into `exited`. A genuine miss prints nothing while a wrapper
failure always explains itself, so require empty stderr alongside the exit
code; a runner that reports no stderr at all keeps its exit-code contract.

That same signal removes a spawn regression: `show-ref` is a direct-git read
under WSL, and the runner retried any numeric exit through the user's
interactive login shell. The replaced `for-each-ref` exited 0 on a miss, so
absence never retried; every absent probe now would. Treat a quiet exit 1 as
Git control flow and skip the fallback.

Also narrow the hosted-review suffix fallback: the replaced
`refs/remotes/*/<base>` could not cross a slash, but `show-ref -- <base>`
matches at any depth, so `origin/feature/main` answered a query for `main`
and submitted a review against a base the provider rejects.

Refresh the real-binary compatibility contract to the shipped excludes, and
assert exact probe concurrency rather than an upper bound so a regression to
serial probing fails.
2026-08-31 16:27:28 -07:00

126 lines
4.7 KiB
TypeScript

import type { GitWorktreeInfo } from '../../shared/worktree/types'
import {
listWorktreeGraph as listWorktreeGraphUnshared,
listWorktreesStrict as listWorktreesStrictUnshared,
listWorktreesUnshared
} from './worktree-listing'
import type { GitWorktreeExecOptions } from './worktree-operation-options'
import { WORKTREE_LIST_TIMEOUT_MS } from './worktree-operation-options'
// Why: share concurrent `git worktree list` scans, which are expensive on Windows.
const inFlightWorktreeScans = new Map<string, Promise<GitWorktreeInfo[]>>()
type WorktreeScanKind = 'graph' | 'lenient' | 'strict'
// Why: mutation generations prevent listings from joining stale scans.
const worktreeScanGenerations = new Map<string, number>()
function hasInFlightWorktreeScanForRepo(repoPath: string): boolean {
const keyPrefix = `${repoPath}\0`
for (const key of inFlightWorktreeScans.keys()) {
if (key.startsWith(keyPrefix)) {
return true
}
}
return false
}
export function bumpWorktreeScanGeneration(repoPath: string): void {
// Why: generations only prevent joining a pre-mutation scan; with no active scan, keeping the repo path just leaks completed mutation keys.
if (!hasInFlightWorktreeScanForRepo(repoPath)) {
return
}
worktreeScanGenerations.set(repoPath, (worktreeScanGenerations.get(repoPath) ?? 0) + 1)
}
function pruneWorktreeScanGeneration(repoPath: string): void {
// Why: keep ordinary scan settlement O(1); only repos invalidated during an active scan need the cross-generation check.
if (!worktreeScanGenerations.has(repoPath)) {
return
}
if (!hasInFlightWorktreeScanForRepo(repoPath)) {
worktreeScanGenerations.delete(repoPath)
}
}
export function _getWorktreeScanCacheSizesForTests(): { inFlight: number; generations: number } {
return {
inFlight: inFlightWorktreeScans.size,
generations: worktreeScanGenerations.size
}
}
export function _resetWorktreeScanCacheForTests(): void {
inFlightWorktreeScans.clear()
worktreeScanGenerations.clear()
}
/**
* Share one in-flight scan per (repo, distro, deadline, generation, kind). Coalescing keeps a
* sidebar refresh from spawning a second `git worktree list` (expensive on Windows), but only
* within one failure discipline: a strict joiner must never inherit a lenient scan's softened `[]`,
* so the strict and lenient runners scan separately by design. The explicit kind is intentional:
* function names can be rewritten by a production bundler and must not define cache identity.
*/
function shareWorktreeScan(
repoPath: string,
options: GitWorktreeExecOptions,
kind: WorktreeScanKind,
run: (repoPath: string, options: GitWorktreeExecOptions) => Promise<GitWorktreeInfo[]>
): Promise<GitWorktreeInfo[]> {
if (options.signal) {
return run(repoPath, options)
}
const generation = worktreeScanGenerations.get(repoPath) ?? 0
const timeout = options.timeout ?? WORKTREE_LIST_TIMEOUT_MS
// Why: callers with different deadlines cannot safely share which timeout wins the scan.
// Why `kind`: a strict joiner must never receive a softened `[]` from a lenient scan.
const key = `${repoPath}\0${options.wslDistro ?? ''}\0${timeout}\0${options.includeCreatePreparations === true}\0${generation}\0${kind}`
const inFlight = inFlightWorktreeScans.get(key)
if (inFlight) {
return inFlight
}
const scan = run(repoPath, options).finally(() => {
if (inFlightWorktreeScans.get(key) === scan) {
inFlightWorktreeScans.delete(key)
}
pruneWorktreeScanGeneration(repoPath)
})
inFlightWorktreeScans.set(key, scan)
return scan
}
/**
* List all worktrees for a git repo at the given path. Concurrent calls for
* the same repo share one scan (unless the caller passes an AbortSignal,
* which must only cancel its own scan). Git failures soften to `[]`.
*/
export function listWorktrees(
repoPath: string,
options: GitWorktreeExecOptions = {}
): Promise<GitWorktreeInfo[]> {
return shareWorktreeScan(repoPath, options, 'lenient', listWorktreesUnshared)
}
/**
* List the worktree graph without sparse-checkout probes. Concurrent callers share the same
* generation-fenced scan, while callers with an AbortSignal retain an isolated subprocess.
*/
export function listWorktreeGraph(
repoPath: string,
options: GitWorktreeExecOptions = {}
): Promise<GitWorktreeInfo[]> {
return shareWorktreeScan(repoPath, options, 'graph', listWorktreeGraphUnshared)
}
/**
* `listWorktreesStrict` through the same in-flight map, so callers that must see a Git failure
* (worktree-create verification) still coalesce with a concurrent refresh (#16520).
*/
export function listWorktreesSharedStrict(
repoPath: string,
options: GitWorktreeExecOptions = {}
): Promise<GitWorktreeInfo[]> {
return shareWorktreeScan(repoPath, options, 'strict', listWorktreesStrictUnshared)
}