mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import { readLocalGitConfigSignature } from '../github/local-git-config-signature'
|
|
import { runCoalescedProbe, type CoalescedProbes } from './coalesced-probe'
|
|
import type { GitAdmissionTier } from './command-runner/git-exec-options'
|
|
import { REMOTE_URL_PROBE_TIMEOUT_MS } from './remote-url-probe'
|
|
import { gitExecFileAsync } from './runner'
|
|
|
|
export type RemoteNameListingGitOptions = {
|
|
wslDistro?: string
|
|
admissionTier?: GitAdmissionTier
|
|
}
|
|
|
|
const SIGNED_REMOTE_NAME_LISTING_TTL_MS = 5 * 60_000
|
|
const UNSIGNED_REMOTE_NAME_LISTING_TTL_MS = 30_000
|
|
const REMOTE_NAME_LISTING_CACHE_MAX_ENTRIES = 512
|
|
|
|
type CachedRemoteNames = {
|
|
remotes: string[]
|
|
expiresAt: number
|
|
configSignature?: string
|
|
}
|
|
|
|
const remoteNameListingCache = new Map<string, CachedRemoteNames>()
|
|
const remoteNameListingInFlight: CoalescedProbes<string[] | null> = new Map()
|
|
|
|
/** @internal - exposed for tests only */
|
|
export function _resetRemoteNameListingCache(): void {
|
|
remoteNameListingCache.clear()
|
|
remoteNameListingInFlight.clear()
|
|
}
|
|
|
|
function parseRemoteNames(stdout: string): string[] {
|
|
return stdout
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function remoteNameListingCacheKey(
|
|
repoPath: string,
|
|
localGitOptions: RemoteNameListingGitOptions = {}
|
|
): string {
|
|
return `local:${localGitOptions.wslDistro ?? 'host'}\0${repoPath}`
|
|
}
|
|
|
|
function pruneRemoteNameListingCache(now: number): void {
|
|
for (const [key, entry] of remoteNameListingCache) {
|
|
if (entry.expiresAt <= now) {
|
|
remoteNameListingCache.delete(key)
|
|
}
|
|
}
|
|
while (remoteNameListingCache.size > REMOTE_NAME_LISTING_CACHE_MAX_ENTRIES) {
|
|
const oldestKey = remoteNameListingCache.keys().next().value
|
|
if (oldestKey === undefined) {
|
|
return
|
|
}
|
|
remoteNameListingCache.delete(oldestKey)
|
|
}
|
|
}
|
|
|
|
function listingGitConfigContext(
|
|
repoPath: string,
|
|
localGitOptions: RemoteNameListingGitOptions = {}
|
|
): { repoPath: string; connectionId: null; wslDistro?: string } {
|
|
return {
|
|
repoPath,
|
|
connectionId: null,
|
|
...(localGitOptions.wslDistro ? { wslDistro: localGitOptions.wslDistro } : {})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `git remote` names for one repo/runtime. Failed listings are not cached: a
|
|
* missed `upstream` would otherwise send issue/PR resolvers to origin on a
|
|
* contributor clone (#7331). An `ssh:` repo is not served here, so it lists as
|
|
* failed (`null`), never as this machine's remotes.
|
|
*/
|
|
export async function listCachedRemoteNames(
|
|
repoPath: string,
|
|
connectionId?: string | null,
|
|
localGitOptions: RemoteNameListingGitOptions = {}
|
|
): Promise<string[] | null> {
|
|
if (connectionId) {
|
|
return null
|
|
}
|
|
const cacheKey = remoteNameListingCacheKey(repoPath, localGitOptions)
|
|
const now = Date.now()
|
|
pruneRemoteNameListingCache(now)
|
|
const cached = remoteNameListingCache.get(cacheKey)
|
|
if (cached && cached.expiresAt > now) {
|
|
if (cached.configSignature !== undefined) {
|
|
const currentSignature = await readLocalGitConfigSignature(
|
|
listingGitConfigContext(repoPath, localGitOptions)
|
|
)
|
|
if (currentSignature === cached.configSignature) {
|
|
return cached.remotes
|
|
}
|
|
remoteNameListingCache.delete(cacheKey)
|
|
} else {
|
|
return cached.remotes
|
|
}
|
|
}
|
|
|
|
return runCoalescedProbe(remoteNameListingInFlight, cacheKey, async (ownsKey) => {
|
|
const configContext = listingGitConfigContext(repoPath, localGitOptions)
|
|
const configSignatureBefore = await readLocalGitConfigSignature(configContext)
|
|
const remotes = await listUncachedRemoteNames(repoPath, localGitOptions)
|
|
if (remotes === null) {
|
|
return null
|
|
}
|
|
if (ownsKey()) {
|
|
const configSignatureAfter = await readLocalGitConfigSignature(configContext)
|
|
const configSignature =
|
|
configSignatureBefore !== undefined && configSignatureBefore === configSignatureAfter
|
|
? configSignatureAfter
|
|
: undefined
|
|
remoteNameListingCache.set(cacheKey, {
|
|
remotes,
|
|
expiresAt:
|
|
Date.now() +
|
|
(configSignature
|
|
? SIGNED_REMOTE_NAME_LISTING_TTL_MS
|
|
: UNSIGNED_REMOTE_NAME_LISTING_TTL_MS),
|
|
...(configSignature ? { configSignature } : {})
|
|
})
|
|
pruneRemoteNameListingCache(Date.now())
|
|
}
|
|
return remotes
|
|
})
|
|
}
|
|
|
|
async function listUncachedRemoteNames(
|
|
repoPath: string,
|
|
localGitOptions: RemoteNameListingGitOptions = {}
|
|
): Promise<string[] | null> {
|
|
try {
|
|
const { stdout } = await gitExecFileAsync(['remote'], {
|
|
cwd: repoPath,
|
|
timeout: REMOTE_URL_PROBE_TIMEOUT_MS,
|
|
...(localGitOptions.wslDistro ? { wslDistro: localGitOptions.wslDistro } : {}),
|
|
...(localGitOptions.admissionTier ? { admissionTier: localGitOptions.admissionTier } : {})
|
|
})
|
|
return parseRemoteNames(stdout)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Probe a named remote only when listing says it exists, or listing failed. */
|
|
export async function shouldProbeGitRemote(
|
|
repoPath: string,
|
|
remoteName: string,
|
|
connectionId?: string | null,
|
|
localGitOptions: RemoteNameListingGitOptions = {}
|
|
): Promise<boolean> {
|
|
const remotes = await listCachedRemoteNames(repoPath, connectionId, localGitOptions)
|
|
return remotes === null || remotes.includes(remoteName)
|
|
}
|