Files
orca/src/relay/git-handler-worktree-list.ts
T
Neil 860ee73a11 fix(git): parse sparse and cquoted paths on the SSH relay (#18389)
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
2026-09-03 02:11:55 -07:00

96 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.312.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)
}