mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
The relay carried its own copies of the worktree-list and unmerged-entry porcelain parsers, and both had drifted from the desktop originals: the relay copy had no `sparse` branch, so SSH sparse checkouts were never marked, and it never C-quote-decoded a conflict path, so a conflicted file with a space or non-ASCII byte was published under its raw quoted name and probed as missing. Move both parsers into src/shared and delete the relay copies, so there is one implementation each. Type the relay's worktree-list plumbing on GitWorktreeInfo instead of Record<string, unknown> so a field-copying step can no longer silently drop a newly parsed field. `isSparse` is a new optional field on the git.listWorktrees result (remote-wire-compatibility Rule 1); Git <2.28 omits the porcelain line and the field stays absent. No new git subcommand or option. Closes #18280
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import { stat } from 'node:fs/promises'
|
||
import type { GitCapabilityCache } from '../shared/git-capability-cache'
|
||
import type { GitExec } from './git-handler-ops'
|
||
import { isUnsupportedWorktreeListZError } from './git-handler-utils'
|
||
import { parseWorktreeList } from '../shared/git-worktree-porcelain-parser'
|
||
import type { GitWorktreeInfo } from '../shared/worktree/types'
|
||
|
||
export type RelayWorktreeInfo = {
|
||
path: string
|
||
branch?: string
|
||
head?: string
|
||
locked?: boolean
|
||
lockReason?: string
|
||
}
|
||
|
||
export async function readRelayWorktreeList(
|
||
git: GitExec,
|
||
repoPath: string,
|
||
capabilities: GitCapabilityCache
|
||
): Promise<RelayWorktreeInfo[]> {
|
||
return capabilities.runWithFallback(
|
||
'worktree-list-z',
|
||
async () => {
|
||
const { stdout } = await git(['worktree', 'list', '--porcelain', '-z'], repoPath)
|
||
return normalizeRelayWorktrees(parseWorktreeList(stdout, { nulDelimited: true }))
|
||
},
|
||
async () => {
|
||
// Why: `-z` preserves newlines; fallback keeps Git <2.36 compatible.
|
||
const { stdout } = await git(['worktree', 'list', '--porcelain'], repoPath)
|
||
return normalizeRelayWorktrees(parseWorktreeList(stdout))
|
||
},
|
||
isUnsupportedWorktreeListZError
|
||
)
|
||
}
|
||
|
||
const PRUNABLE_EXISTENCE_PROBE_CONCURRENCY = 8
|
||
|
||
/** Why: Git <2.31 does not emit the `prunable` porcelain annotation, so probe
|
||
* each linked worktree path directly instead of treating a stale registration
|
||
* as a live workspace (issue #8389). Runs on the `-z`-unsupported fallback
|
||
* (Git <2.36); on Git 2.31–2.35 the annotation is already parsed, so this is a
|
||
* harmless backstop. The relay owns the filesystem, so a plain stat is
|
||
* authoritative. */
|
||
export async function annotatePrunableWorktreesByExistence(
|
||
worktrees: GitWorktreeInfo[]
|
||
): Promise<GitWorktreeInfo[]> {
|
||
const annotated = [...worktrees]
|
||
let nextIndex = 0
|
||
|
||
async function probeNext(): Promise<void> {
|
||
while (nextIndex < worktrees.length) {
|
||
const index = nextIndex
|
||
nextIndex += 1
|
||
const worktree = worktrees[index]
|
||
const worktreePath = worktree?.path ?? ''
|
||
// Git only marks linked worktrees prunable, and never locked ones (a
|
||
// lock shields the registration even when the directory is missing). The
|
||
// `locked` annotation is only parsed on Git >=2.31, so on older Git a
|
||
// locked+missing worktree cannot be shielded here. A missing main
|
||
// worktree is surfaced by the repo-level failure paths.
|
||
if (
|
||
!worktreePath ||
|
||
worktree.isMainWorktree === true ||
|
||
worktree.isBare === true ||
|
||
worktree.locked === true ||
|
||
worktree.prunable === true
|
||
) {
|
||
continue
|
||
}
|
||
try {
|
||
await stat(worktreePath)
|
||
} catch (err) {
|
||
if ((err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
||
annotated[index] = { ...worktree, prunable: true }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const workerCount = Math.min(PRUNABLE_EXISTENCE_PROBE_CONCURRENCY, worktrees.length)
|
||
await Promise.all(Array.from({ length: workerCount }, () => probeNext()))
|
||
return annotated
|
||
}
|
||
|
||
function normalizeRelayWorktrees(worktrees: GitWorktreeInfo[]): RelayWorktreeInfo[] {
|
||
return worktrees
|
||
.map((worktree) => ({
|
||
path: worktree.path,
|
||
head: worktree.head,
|
||
branch: worktree.branch,
|
||
locked: worktree.locked === true ? true : undefined,
|
||
lockReason: worktree.lockReason
|
||
}))
|
||
.filter((worktree) => worktree.path.length > 0)
|
||
}
|