mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
The preparation lock reason named only the owner's pid, so a host sharing the repo's disk (an NFS home) read another machine's in-progress preparation as abandoned and discarded it. The reason now carries the writer's machine identity (optional, appended after the fields older builds read); a different or unknown machine is unverifiable, and a reason without one is judged by pid only on a local disk.
64 lines
2.7 KiB
TypeScript
64 lines
2.7 KiB
TypeScript
export const WORKTREE_CREATE_PREPARATION_DIRECTORY = '.orca-preparing'
|
|
export const WORKTREE_CREATE_PREPARATION_LOCK_PREFIX = 'orca-create-preparation:v1:'
|
|
const WORKTREE_CREATE_PREPARATION_ID_PATTERN =
|
|
/^(\d+)-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
|
|
// Appended last so older builds, which read only the pid field, still parse the reason.
|
|
const WORKTREE_CREATE_PREPARATION_MACHINE_FIELD = ':machine='
|
|
|
|
/** The machine is what makes the pid meaningful: a repo on a shared disk is seen by other hosts. */
|
|
export function createWorktreePreparationLockReason(
|
|
sessionId: string,
|
|
machineIdentity?: string | null
|
|
): string {
|
|
const machine = machineIdentity
|
|
? `${WORKTREE_CREATE_PREPARATION_MACHINE_FIELD}${machineIdentity}`
|
|
: ''
|
|
return `${WORKTREE_CREATE_PREPARATION_LOCK_PREFIX}${process.pid}:${sessionId}${machine}`
|
|
}
|
|
|
|
/** Null for reasons older builds wrote, or a writer that could not read its machine identity. */
|
|
export function parseWorktreePreparationOwnerMachine(lockReason?: string): string | null {
|
|
if (!lockReason || parseWorktreePreparationOwnerPid(lockReason) === null) {
|
|
return null
|
|
}
|
|
const at = lockReason.indexOf(WORKTREE_CREATE_PREPARATION_MACHINE_FIELD)
|
|
return at === -1
|
|
? null
|
|
: lockReason.slice(at + WORKTREE_CREATE_PREPARATION_MACHINE_FIELD.length) || null
|
|
}
|
|
|
|
export function parseWorktreePreparationOwnerPid(lockReason?: string): number | null {
|
|
if (!lockReason?.startsWith(WORKTREE_CREATE_PREPARATION_LOCK_PREFIX)) {
|
|
return null
|
|
}
|
|
const pid = Number(lockReason.slice(WORKTREE_CREATE_PREPARATION_LOCK_PREFIX.length).split(':')[0])
|
|
return Number.isSafeInteger(pid) && pid > 0 ? pid : null
|
|
}
|
|
|
|
export function parseWorktreePreparationPathOwnerPid(path: string): number | null {
|
|
const pathParts = path.split(/[\\/]+/)
|
|
const preparationIndex = pathParts.lastIndexOf(WORKTREE_CREATE_PREPARATION_DIRECTORY)
|
|
if (preparationIndex === -1) {
|
|
return null
|
|
}
|
|
const preparationId = pathParts[preparationIndex + 1]
|
|
if (!preparationId || !WORKTREE_CREATE_PREPARATION_ID_PATTERN.test(preparationId)) {
|
|
return null
|
|
}
|
|
const pid = Number(preparationId.split('-')[0])
|
|
return Number.isSafeInteger(pid) && pid > 0 ? pid : null
|
|
}
|
|
|
|
export function isWorktreeCreatePreparation(worktree: {
|
|
path: string
|
|
lockReason?: string
|
|
branch?: string
|
|
}): boolean {
|
|
// The Git lock reason is the durable ownership proof. A path can be chosen
|
|
// by a user (including for an uncommitted detached worktree), so path shape
|
|
// alone must never hide or force-remove it. A crash before locking may leave
|
|
// an unlocked detached entry for manual cleanup, but cannot delete user data.
|
|
return parseWorktreePreparationOwnerPid(worktree.lockReason) !== null
|
|
}
|