mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
* Split speech session lifecycle * Split terminal output scheduler pipeline * Split mobile browser pane modules * Prune resolved max-lines suppressions * Split pane tree equalization logic * Extract mobile troubleshoot screen styles * Split external automation manager * Split main window service attachments * Split hosted review creation checks * Split automation dispatch event handling * Split settings navigation metadata * Split daemon initialization lifecycle * Split GitLab item dialog * Split relay dispatcher layers * Split mobile host screen * Retarget mobile view settings source test * Split runtime file client layers * Split ports panel layers * Split runtime environments pane layers * Split local PTY provider responsibilities * Split CDP bridge responsibilities * Split relay Git handler responsibilities * Track moved relay Git fetch audit * Split Linear item drawer responsibilities * Split telemetry event schema responsibilities * Split resource usage status responsibilities * Split remote terminal multiplexer responsibilities * Split Git worktree responsibilities * Fix F3-speech for #17123 * Fix F1-cycle for #17131 * Fix F4-navtest for #17157 * Fix F2-allowlist for #17161
69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
import { gitExecFileAsync } from './runner'
|
|
import type { WorktreeRemovalPreflightOptions } from './worktree-operation-options'
|
|
import { WORKTREE_REMOVAL_PREFLIGHT_TIMEOUT_MS, gitExecOptions } from './worktree-operation-options'
|
|
|
|
/**
|
|
* Assert a worktree is clean enough for non-force removal.
|
|
*/
|
|
export async function assertWorktreeCleanForRemoval(
|
|
worktreePath: string,
|
|
force = false,
|
|
options: WorktreeRemovalPreflightOptions = {}
|
|
): Promise<void> {
|
|
if (force) {
|
|
return
|
|
}
|
|
|
|
const { ignoredUntrackedPaths = [], ...gitOptions } = options
|
|
const useNullTerminatedStatus = ignoredUntrackedPaths.length > 0
|
|
const { stdout } = await gitExecFileAsync(
|
|
['status', '--porcelain', ...(useNullTerminatedStatus ? ['-z'] : []), '--untracked-files=all'],
|
|
{
|
|
...gitExecOptions(worktreePath, gitOptions),
|
|
timeout: gitOptions.timeout ?? WORKTREE_REMOVAL_PREFLIGHT_TIMEOUT_MS
|
|
}
|
|
)
|
|
// Why one parse feeds both: the clean verdict and the error text must never
|
|
// disagree about which entries block removal.
|
|
const blockingEntries = useNullTerminatedStatus
|
|
? getBlockingUntrackedStatusEntries(stdout, ignoredUntrackedPaths)
|
|
: null
|
|
if (blockingEntries ? blockingEntries.length === 0 : !stdout.trim()) {
|
|
return
|
|
}
|
|
|
|
const error = new Error('Worktree has uncommitted or untracked changes.')
|
|
// Why not the raw stdout: `-z` output is NUL-delimited and `.trim()` leaves
|
|
// interior NULs, so attaching it verbatim put raw control bytes into the
|
|
// user-facing removal error — and listed the tolerated shared link, the one
|
|
// entry that is not the user's work and cannot be committed away.
|
|
;(error as Error & { stdout?: string }).stdout = blockingEntries
|
|
? blockingEntries.join('\n')
|
|
: stdout
|
|
throw error
|
|
}
|
|
|
|
/** The `git status --porcelain -z` entries that genuinely block removal:
|
|
* everything except the untracked shared links the caller tolerates. */
|
|
function getBlockingUntrackedStatusEntries(
|
|
status: string,
|
|
ignoredUntrackedPaths: readonly string[]
|
|
): string[] {
|
|
const ignored = new Set(
|
|
ignoredUntrackedPaths
|
|
.map((entry) =>
|
|
entry
|
|
.trim()
|
|
.replace(/^[\\/]+/, '')
|
|
.replace(/\\/g, '/')
|
|
)
|
|
.filter((entry) => entry && !entry.split('/').includes('..'))
|
|
)
|
|
return status
|
|
.split('\0')
|
|
.filter(Boolean)
|
|
.filter(
|
|
(entry) => !(entry.startsWith('?? ') && ignored.has(entry.slice(3).replace(/\\/g, '/')))
|
|
)
|
|
}
|