Files
orca/src/shared/git-capability-cache.ts
T
Neil 533992bdda fix(git): cache unsupported capabilities per host (#8109)
* fix(git): cache unsupported capabilities per host

Old Git worktree, ref-search, and merge-tree fallbacks retried unsupported flags on recurring operations, flooding subprocess traces. Centralize capability probing per native, WSL, and SSH execution host, coalesce concurrent probes, and retry periodically for in-place Git upgrades.

* fix(git): recognize real old-Git merge-tree rejection

* test(git): enforce real binary compatibility matrix

* fix(ci): preserve Git compatibility test ownership

* fix(git): retain supported capability state
2026-07-10 18:19:36 -07:00

116 lines
3.9 KiB
TypeScript

// Why: suppress hot-loop failures while still detecting an in-place Git
// upgrade during a long Orca session without requiring a restart.
export const GIT_CAPABILITY_RETRY_INTERVAL_MS = 30 * 60_000
export type GitCapability =
| 'for-each-ref-exclude'
| 'merge-tree-merge-base'
| 'merge-tree-write-tree'
| 'rev-parse-path-format'
| 'worktree-list-z'
type GitCapabilityProbeOutcome = 'supported' | 'unsupported' | 'unknown'
export class GitCapabilityCache {
private readonly retryAfterByCapability = new Map<GitCapability, number>()
private readonly probesByCapability = new Map<GitCapability, Promise<GitCapabilityProbeOutcome>>()
private readonly supportedCapabilities = new Set<GitCapability>()
shouldTry(capability: GitCapability, nowMs = Date.now()): boolean {
const retryAfterMs = this.retryAfterByCapability.get(capability)
if (retryAfterMs === undefined) {
return true
}
if (nowMs < retryAfterMs) {
return false
}
this.retryAfterByCapability.delete(capability)
return true
}
rememberUnsupported(capability: GitCapability, nowMs = Date.now()): void {
// Why: optimistic probes preserve newer Git behavior, but repeating a
// known failure on every poll/search wastes subprocesses and trace space.
this.supportedCapabilities.delete(capability)
this.retryAfterByCapability.set(capability, nowMs + GIT_CAPABILITY_RETRY_INTERVAL_MS)
}
async runWithFallback<T>(
capability: GitCapability,
runPreferred: () => Promise<T>,
runFallback: () => Promise<T>,
isUnsupportedError: (error: unknown) => boolean
): Promise<T> {
if (this.supportedCapabilities.has(capability)) {
// Why: supported commands are real work, not disposable probes. Let
// sibling repo/SSH calls retain their intended concurrency.
return this.runPreferredOrFallback(capability, runPreferred, runFallback, isUnsupportedError)
}
if (!this.shouldTry(capability)) {
return runFallback()
}
const inFlightProbe = this.probesByCapability.get(capability)
if (inFlightProbe) {
const outcome = await inFlightProbe
if (outcome === 'unsupported' || !this.shouldTry(capability)) {
return runFallback()
}
return this.runPreferredOrFallback(capability, runPreferred, runFallback, isUnsupportedError)
}
let settleProbe!: (outcome: GitCapabilityProbeOutcome) => void
const probe = new Promise<GitCapabilityProbeOutcome>((resolve) => {
settleProbe = resolve
})
this.probesByCapability.set(capability, probe)
try {
return await this.runPreferredOrFallback(
capability,
runPreferred,
runFallback,
isUnsupportedError,
settleProbe
)
} finally {
if (this.probesByCapability.get(capability) === probe) {
this.probesByCapability.delete(capability)
}
}
}
clear(): void {
this.retryAfterByCapability.clear()
this.probesByCapability.clear()
this.supportedCapabilities.clear()
}
private async runPreferredOrFallback<T>(
capability: GitCapability,
runPreferred: () => Promise<T>,
runFallback: () => Promise<T>,
isUnsupportedError: (error: unknown) => boolean,
settleProbe?: (outcome: GitCapabilityProbeOutcome) => void
): Promise<T> {
try {
const result = await runPreferred()
// A preferred callback can detect old Git's exit-zero option echo and
// remember it as unsupported, so do not overwrite that stronger signal.
const outcome = this.retryAfterByCapability.has(capability) ? 'unsupported' : 'supported'
if (outcome === 'supported') {
this.supportedCapabilities.add(capability)
}
settleProbe?.(outcome)
return result
} catch (error) {
if (!isUnsupportedError(error)) {
settleProbe?.('unknown')
throw error
}
this.rememberUnsupported(capability)
settleProbe?.('unsupported')
return runFallback()
}
}
}