mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci] * [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci] * [comment audit] Tighten Git execution and source-control comment contracts [skip ci] * [comment audit] Clarify repository probes and maintenance comments [skip ci] * [comment audit] Shorten status, diff-cache, and checkout comments [skip ci] * [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci] * [comment audit] Shorten forge identity and review-refresh comments [skip ci] * [comment audit] Tighten hosted-review client comment contracts [skip ci] * [comment audit] Clarify GitLab compatibility and review comments [skip ci] * [comment audit] Tighten hook ownership and watchdog rationale [skip ci] * [comment audit] Condense host and integration compatibility comments [skip ci] * [comment audit] Clarify IPC authority and remote-routing rationale [skip ci] * [comment audit] Finalize file-import and containment comment cleanup * [comment audit] Fix stale Jira type imports blocking full CI --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
import { throwIfSignalAborted, waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
|
|
import { readFile, realpath, stat } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence'
|
|
import { resolveGitMetadataPath } from '../../shared/git-metadata-path'
|
|
import { parseGitdirMarkerPayload } from '../../shared/gitdir-marker-payload'
|
|
import { isWorktreeCreatePreparation } from '../../shared/worktree/create-preparation'
|
|
import { toWslExecutionSpace } from '../../shared/wsl-paths'
|
|
import type { GitWorktreeInfo } from '../../shared/worktree/types'
|
|
import {
|
|
readCheckedOutBranchRef,
|
|
readRepoCommonDirFromGit,
|
|
readRepoLocation,
|
|
readTranslatedWorktreeGraph,
|
|
readWorktreeHeadOid,
|
|
readWorktreeList
|
|
} from './worktree-list-reader'
|
|
import type { GitWorktreeExecOptions } from './worktree-operation-options'
|
|
import {
|
|
WORKTREE_LIST_TIMEOUT_MS,
|
|
getErrorCode,
|
|
isNotGitRepositoryError,
|
|
normalizeLocalBranchRef
|
|
} from './worktree-operation-options'
|
|
import { areWorktreePathsEqual, translateWorktreePath } from './worktree-path-comparison'
|
|
import { detectSparseCheckoutCached } from './worktree-sparse-checkout-cache'
|
|
|
|
const SPARSE_CHECKOUT_DETECTION_CONCURRENCY = 8
|
|
|
|
export async function listWorktreeGraph(
|
|
repoPath: string,
|
|
options: GitWorktreeExecOptions = {}
|
|
): Promise<GitWorktreeInfo[]> {
|
|
try {
|
|
const worktrees = await readTranslatedWorktreeGraph(repoPath, options)
|
|
return options.includeCreatePreparations
|
|
? worktrees
|
|
: worktrees.filter((worktree) => !isWorktreeCreatePreparation(worktree))
|
|
} catch (err) {
|
|
if (await isTrueEmptyWorktreeListing(repoPath, err)) {
|
|
return []
|
|
}
|
|
console.warn(`[git/worktree] listWorktreeGraph failed for ${repoPath}:`, err)
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function listWorktreesUnshared(
|
|
repoPath: string,
|
|
options: GitWorktreeExecOptions = {}
|
|
): Promise<GitWorktreeInfo[]> {
|
|
try {
|
|
const worktrees = await readTranslatedWorktreeGraph(repoPath, options)
|
|
const visibleWorktrees = options.includeCreatePreparations
|
|
? worktrees
|
|
: worktrees.filter((worktree) => !isWorktreeCreatePreparation(worktree))
|
|
return annotateSparseCheckoutStatus(repoPath, visibleWorktrees, options)
|
|
} catch (err) {
|
|
if (await isTrueEmptyWorktreeListing(repoPath, err)) {
|
|
return []
|
|
}
|
|
console.warn(`[git/worktree] listWorktrees failed for ${repoPath}:`, err)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/** Only a missing path or confirmed non-repo permits an authoritative empty listing. */
|
|
async function isTrueEmptyWorktreeListing(repoPath: string, err: unknown): Promise<boolean> {
|
|
if (getErrorCode(err) === 'ENOENT') {
|
|
try {
|
|
await stat(repoPath)
|
|
} catch (statErr) {
|
|
if (getErrorCode(statErr) === 'ENOENT') {
|
|
console.warn(`[git/worktree] repo path missing; skipping worktree list: ${repoPath}`)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return isNotGitRepositoryError(err)
|
|
}
|
|
|
|
export async function listWorktreesStrict(
|
|
repoPath: string,
|
|
options: GitWorktreeExecOptions = {}
|
|
): Promise<GitWorktreeInfo[]> {
|
|
const worktrees = (await readWorktreeList(repoPath, options)).map((worktree) => {
|
|
const translatedPath = translateWorktreePath(worktree.path, repoPath, options)
|
|
return translatedPath === worktree.path ? worktree : { ...worktree, path: translatedPath }
|
|
})
|
|
const visibleWorktrees = options.includeCreatePreparations
|
|
? worktrees
|
|
: worktrees.filter((worktree) => !isWorktreeCreatePreparation(worktree))
|
|
return annotateSparseCheckoutStatus(repoPath, visibleWorktrees, options)
|
|
}
|
|
|
|
/** Reject scan failures to prevent pruning valid worktrees and tabs; confirmed missing/non-repos return []. */
|
|
export async function listWorktreesStrictAllowingTrueEmpty(
|
|
repoPath: string,
|
|
options: GitWorktreeExecOptions = {}
|
|
): Promise<GitWorktreeInfo[]> {
|
|
try {
|
|
return await listWorktreesStrict(repoPath, options)
|
|
} catch (err) {
|
|
if (await isTrueEmptyWorktreeListing(repoPath, err)) {
|
|
return []
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
export async function annotateSparseCheckoutStatus(
|
|
repoPath: string,
|
|
worktrees: GitWorktreeInfo[],
|
|
options: GitWorktreeExecOptions = {}
|
|
): Promise<GitWorktreeInfo[]> {
|
|
const annotated = [...worktrees]
|
|
let nextIndex = 0
|
|
|
|
async function detectNext(): Promise<void> {
|
|
while (nextIndex < worktrees.length) {
|
|
throwIfSignalAborted(options.signal)
|
|
const index = nextIndex
|
|
nextIndex += 1
|
|
const worktree = worktrees[index]
|
|
if (!worktree || worktree.isBare || worktree.isSparse) {
|
|
continue
|
|
}
|
|
const isSparse = await detectSparseCheckoutCached(repoPath, worktree.path, options)
|
|
throwIfSignalAborted(options.signal)
|
|
if (isSparse) {
|
|
annotated[index] = { ...worktree, isSparse }
|
|
}
|
|
}
|
|
}
|
|
|
|
const workerCount = Math.min(SPARSE_CHECKOUT_DETECTION_CONCURRENCY, worktrees.length)
|
|
await waitForPromiseWithSignal(
|
|
Promise.all(Array.from({ length: workerCount }, () => detectNext())),
|
|
options.signal
|
|
)
|
|
throwIfSignalAborted(options.signal)
|
|
return annotated
|
|
}
|
|
|
|
/** Bound the disk witness on hung mounts; absent .git returns undefined, other read failures reject. */
|
|
async function readRepoCommonDirFromDisk(
|
|
repoPath: string,
|
|
timeoutMs: number
|
|
): Promise<string | undefined> {
|
|
const dotGit = join(repoPath, '.git')
|
|
try {
|
|
const commonDir = await withDeadline(resolveRepoCommonDirFromDisk(repoPath, dotGit), timeoutMs)
|
|
return commonDir ? toWslExecutionSpace(commonDir) : undefined
|
|
} catch (error) {
|
|
// A bare repo has no `.git`; do not fabricate a candidate for it.
|
|
if (isDefinitiveAbsence(error)) {
|
|
return undefined
|
|
}
|
|
const reason = error instanceof Error ? error.message : String(error)
|
|
throw new Error(`repo common dir unverifiable: could not read ${dotGit}: ${reason}`, {
|
|
cause: error
|
|
})
|
|
}
|
|
}
|
|
|
|
async function resolveRepoCommonDirFromDisk(
|
|
repoPath: string,
|
|
dotGit: string
|
|
): Promise<string | undefined> {
|
|
// The general metadata resolvers are intentionally best effort; a witness must preserve read failures.
|
|
const dotGitStats = await stat(dotGit)
|
|
let gitDir = dotGit
|
|
if (!dotGitStats.isDirectory()) {
|
|
const pointer = parseGitdirMarkerPayload(await readFile(dotGit, 'utf8'))
|
|
if (!pointer) {
|
|
return undefined
|
|
}
|
|
gitDir = resolveGitMetadataPath(repoPath, pointer) ?? dotGit
|
|
await assertGitDirIsDirectory(gitDir)
|
|
}
|
|
|
|
return readCommonDirMarker(gitDir)
|
|
}
|
|
|
|
/** Reject unreadable marker targets before commondir absence can be mistaken for a bare repo. */
|
|
async function assertGitDirIsDirectory(gitDir: string): Promise<void> {
|
|
let gitDirStats
|
|
try {
|
|
gitDirStats = await stat(gitDir)
|
|
} catch (error) {
|
|
// Hide this errno from the outer missing-.git check.
|
|
throw new Error(`gitdir marker target unreadable: ${gitDir}`, { cause: error })
|
|
}
|
|
if (!gitDirStats.isDirectory()) {
|
|
throw new Error(`gitdir marker target is not a directory: ${gitDir}`)
|
|
}
|
|
}
|
|
|
|
async function readCommonDirMarker(gitDir: string): Promise<string> {
|
|
try {
|
|
const pointer = await readFile(join(gitDir, 'commondir'), 'utf8')
|
|
return resolveGitMetadataPath(gitDir, pointer) ?? gitDir
|
|
} catch (error) {
|
|
if (!isDefinitiveAbsence(error)) {
|
|
throw error
|
|
}
|
|
return gitDir
|
|
}
|
|
}
|
|
|
|
async function withDeadline<T>(work: Promise<T>, timeoutMs: number): Promise<T> {
|
|
let timer: NodeJS.Timeout | undefined
|
|
try {
|
|
return await Promise.race([
|
|
work,
|
|
new Promise<never>((_, reject) => {
|
|
timer = setTimeout(() => reject(new Error('deadline exceeded')), timeoutMs)
|
|
})
|
|
])
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
async function canonicalizeLocalPath(pathValue: string): Promise<string> {
|
|
try {
|
|
return await realpath(pathValue)
|
|
} catch {
|
|
// Paths from another execution space may have no local inode.
|
|
return pathValue
|
|
}
|
|
}
|
|
|
|
/** Git and disk witnesses may differ through symlinks or WSL path spelling; accept any matching candidate. */
|
|
async function isSameRepoCommonDir(
|
|
createdCommonDir: string,
|
|
candidates: readonly (string | undefined)[]
|
|
): Promise<boolean> {
|
|
const present = candidates.filter((candidate): candidate is string => Boolean(candidate))
|
|
if (present.some((candidate) => areWorktreePathsEqual(createdCommonDir, candidate))) {
|
|
return true
|
|
}
|
|
const [canonicalCreated, ...canonicalCandidates] = await Promise.all(
|
|
[createdCommonDir, ...present].map(canonicalizeLocalPath)
|
|
)
|
|
return canonicalCandidates.some((candidate) => areWorktreePathsEqual(canonicalCreated, candidate))
|
|
}
|
|
|
|
/** Recover an omitted listing row only when Git confirms the expected branch, HEAD and repo object store. */
|
|
export async function describeCreatedWorktree(
|
|
repoPath: string,
|
|
worktreePath: string,
|
|
branch: string,
|
|
options: GitWorktreeExecOptions = {}
|
|
): Promise<GitWorktreeInfo | undefined> {
|
|
const expectedRef = `refs/heads/${normalizeLocalBranchRef(branch)}`
|
|
// Bound Git recovery separately; filesystem canonicalization remains best effort.
|
|
const deadlined: GitWorktreeExecOptions = {
|
|
...options,
|
|
timeout: options.timeout ?? WORKTREE_LIST_TIMEOUT_MS
|
|
}
|
|
const [created, repoGitCommonDir, checkedOutRef, head] = await Promise.all([
|
|
readRepoLocation(worktreePath, toWslExecutionSpace(worktreePath), deadlined),
|
|
readRepoCommonDirFromGit(repoPath, deadlined),
|
|
readCheckedOutBranchRef(worktreePath, deadlined),
|
|
readWorktreeHeadOid(worktreePath, deadlined)
|
|
])
|
|
if (!created || checkedOutRef !== expectedRef || !head) {
|
|
return undefined
|
|
}
|
|
if (!(await isSameRepoCommonDir(created.commonDir, [repoGitCommonDir]))) {
|
|
// Defer disk witnesses: a hung mount can pin a threadpool thread even after the deadline.
|
|
const repoDiskCommonDir = await readRepoCommonDirFromDisk(
|
|
repoPath,
|
|
deadlined.timeout ?? WORKTREE_LIST_TIMEOUT_MS
|
|
)
|
|
if (!(await isSameRepoCommonDir(created.commonDir, [repoDiskCommonDir]))) {
|
|
return undefined
|
|
}
|
|
}
|
|
const [described] = await annotateSparseCheckoutStatus(
|
|
repoPath,
|
|
[
|
|
{
|
|
path: translateWorktreePath(created.topLevel, repoPath, options),
|
|
head,
|
|
branch: expectedRef,
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
],
|
|
options
|
|
)
|
|
return described
|
|
}
|