Files
orca/src/shared/git-progress-records.ts
T
Brennan Benson 131986aa16 feat(worktree): show checkout progress while a worktree is created (#24093)
* feat(worktree): show checkout progress while a worktree is created

A local create spends most of its time in the checkout `git worktree add`
performs, and the card only said "Creating worktree…" for that whole time.
Git already writes its "Updating files: N%" meter into our stderr pipe, so
the add call reads it passively (same command line and environment) and the
create card shows "Checking out files… N%" with a thin bar. All six create
labels are translated so a create never switches language mid-flight.

Rebuilt on main from the version stacked on the create-gating branch. On
main a create can also claim a prepared checkout; that path never runs
`git worktree add`, so it keeps today's "Creating worktree…" label with no
bar, and a missed claim falls back to the plain add, which shows the bar.

* test(worktree): pin that finalizing a prepared checkout reports no checkout progress

Claiming a prepared checkout never runs `git worktree add`, so the create
card keeps its label with no bar; this pins that at the finalize step,
including the retarget `reset --hard`.

* test(worktree): keep only the checkout-progress tests that pin a contract

Drops unit tests that mirror the implementation; the real-git flow, the parser
captures, the cross-version Git contract and the create card test remain.

* test(worktree): pin the checkout meter's path from git to the create card

* test(worktree): merge the narrow-terminal capture's pipe chunks
2026-10-15 18:14:56 -07:00

70 lines
1.9 KiB
TypeScript

/** One `Title: NN% (x/y)` record from git's stderr progress meter. */
export type GitProgressRecord = {
percent: number
completed: number
total: number
/** The final `, done.` record. */
done: boolean
}
const COUNTERS = /^\s*(\d+)% \((\d+)\/(\d+)\)(, done\.)?/
// Why: git redraws with a bare `\r` and only ends a line when done, so a stream
// without separators is never progress; keeping its tail bounds the buffer.
const MAX_PENDING_CHARS = 256
/**
* Reads git's progress meter for one title out of stderr chunks. Records may
* span chunks; everything else on stderr is ignored.
*/
export function createGitProgressRecordReader(
title: string,
onRecord: (record: GitProgressRecord) => void
): (chunk: string) => void {
const prefix = `${title}:`
let pending = ''
// Why: when the title and counters are wider than COLUMNS (80 by default in
// a pipe), git prints the title once on its own line and then bare counters.
let titleOnOwnLine = false
const readLine = (line: string): void => {
if (line === '') {
return
}
if (line.startsWith(prefix)) {
const counters = line.slice(prefix.length)
titleOnOwnLine = counters.trim() === ''
readCounters(counters)
return
}
if (titleOnOwnLine && !readCounters(line)) {
titleOnOwnLine = false
}
}
const readCounters = (text: string): boolean => {
const match = COUNTERS.exec(text)
if (!match) {
return false
}
const total = Number(match[3])
if (total <= 0) {
return false
}
onRecord({
percent: Math.min(100, Number(match[1])),
completed: Number(match[2]),
total,
done: match[4] !== undefined
})
return true
}
return (chunk) => {
const lines = (pending + chunk).split(/\r\n|\r|\n/)
pending = (lines.pop() ?? '').slice(-MAX_PENDING_CHARS)
for (const line of lines) {
readLine(line)
}
}
}