mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 00:02:41 +00:00
* 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.
126 lines
4.7 KiB
TypeScript
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)
|
|
}
|