mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +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
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
import { decodeGitCQuotedPath } from './git-cquoted-path'
|
|
import type { GitWorktreeInfo } from './worktree/types'
|
|
|
|
/**
|
|
* Parse the porcelain output of `git worktree list --porcelain`.
|
|
*/
|
|
export function parseWorktreeList(
|
|
output: string,
|
|
options: { nulDelimited?: boolean } = {}
|
|
): GitWorktreeInfo[] {
|
|
const worktrees: GitWorktreeInfo[] = []
|
|
const blocks = options.nulDelimited ? splitNulWorktreeList(output) : splitLineWorktreeList(output)
|
|
|
|
for (const lines of blocks) {
|
|
if (lines.length === 0) {
|
|
continue
|
|
}
|
|
|
|
let path = ''
|
|
let head = ''
|
|
let branch = ''
|
|
let isBare = false
|
|
let isSparse = false
|
|
let locked = false
|
|
let lockReason = ''
|
|
let prunable = false
|
|
let prunableReason = ''
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('worktree ')) {
|
|
path = line.slice('worktree '.length)
|
|
} else if (line.startsWith('HEAD ')) {
|
|
head = line.slice('HEAD '.length)
|
|
} else if (line.startsWith('branch ')) {
|
|
branch = line.slice('branch '.length)
|
|
} else if (line === 'bare') {
|
|
isBare = true
|
|
} else if (line === 'sparse') {
|
|
isSparse = true
|
|
} else if (line === 'locked' || line.startsWith('locked ')) {
|
|
locked = true
|
|
const rawReason = line.slice('locked'.length).trim()
|
|
lockReason = options.nulDelimited ? rawReason : decodeGitCQuotedPath(rawReason)
|
|
} else if (line === 'prunable' || line.startsWith('prunable ')) {
|
|
// Why: Git ≥2.36 flags registrations whose directory is gone; ignoring it shows the stale worktree as live (#8389).
|
|
prunable = true
|
|
const rawReason = line.slice('prunable'.length).trim()
|
|
prunableReason = options.nulDelimited ? rawReason : decodeGitCQuotedPath(rawReason)
|
|
}
|
|
}
|
|
|
|
if (path) {
|
|
// `git worktree list` always emits the main working tree first.
|
|
worktrees.push({
|
|
path,
|
|
head,
|
|
branch,
|
|
isBare,
|
|
...(isSparse ? { isSparse } : {}),
|
|
...(locked ? { locked: true } : {}),
|
|
...(lockReason ? { lockReason } : {}),
|
|
...(prunable ? { prunable: true } : {}),
|
|
...(prunableReason ? { prunableReason } : {}),
|
|
isMainWorktree: worktrees.length === 0
|
|
})
|
|
}
|
|
}
|
|
|
|
return worktrees
|
|
}
|
|
|
|
function splitLineWorktreeList(output: string): string[][] {
|
|
return output
|
|
.trim()
|
|
.split(/\r?\n\r?\n/)
|
|
.map((block) => block.trim().split(/\r?\n/))
|
|
}
|
|
|
|
function splitNulWorktreeList(output: string): string[][] {
|
|
if (!output.includes('\0')) {
|
|
return splitLineWorktreeList(output)
|
|
}
|
|
|
|
const blocks: string[][] = []
|
|
let currentBlock: string[] = []
|
|
|
|
for (const field of output.split('\0')) {
|
|
if (field) {
|
|
currentBlock.push(field)
|
|
continue
|
|
}
|
|
if (currentBlock.length > 0) {
|
|
blocks.push(currentBlock)
|
|
currentBlock = []
|
|
}
|
|
}
|
|
|
|
if (currentBlock.length > 0) {
|
|
blocks.push(currentBlock)
|
|
}
|
|
|
|
return blocks
|
|
}
|