Files
orca/src/main/git/worktree-create-preparation.ts
T
OrcaWinandOrcaWin 4687e90a16 [comment audit] Continue main-process comment cleanup (#32946)
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci]

* [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci]

* [comment audit] Tighten Git execution and source-control comment contracts [skip ci]

* [comment audit] Clarify repository probes and maintenance comments [skip ci]

* [comment audit] Shorten status, diff-cache, and checkout comments [skip ci]

* [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci]

* [comment audit] Shorten forge identity and review-refresh comments [skip ci]

* [comment audit] Tighten hosted-review client comment contracts [skip ci]

* [comment audit] Clarify GitLab compatibility and review comments [skip ci]

* [comment audit] Tighten hook ownership and watchdog rationale [skip ci]

* [comment audit] Condense host and integration compatibility comments [skip ci]

* [comment audit] Clarify IPC authority and remote-routing rationale [skip ci]

* [comment audit] Finalize file-import and containment comment cleanup

* [comment audit] Fix stale Jira type imports blocking full CI

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 16:27:37 -07:00

281 lines
9.7 KiB
TypeScript

import { windowsLongPathGitArgs } from '../../shared/windows-long-path-git-args'
import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import type { AddWorktreeOptions, AddWorktreeResult, GitWorktreeExecOptions } from './worktree'
import { gitExecOptions } from './worktree-operation-options'
import {
configurePushAutoSetupRemote,
notifyPreparedWorktreeMutation,
persistWorktreeCreationBase,
resolveWorktreeAddBaseContext,
resolveWorktreeAddTimeoutMs,
trackSameNameRemoteBase,
WORKTREE_REMOVAL_REGISTRATION_TIMEOUT_MS
} from './worktree'
import {
gitCleanupOptions,
performDiscardPreparedWorktree,
removeFailedFinalization
} from './worktree-preparation-discard'
import { hasWorktreeBaseCommitRef } from './worktree-base-ref-probe'
import { withRepoRefMaintenancePaused } from './local-repo-ref-maintenance'
import { gitExecFileAsync } from './runner'
import { runWithGitReadCacheInvalidation } from './status'
import { invalidateWslLinkedWorktreeGitRouting } from './wsl-linked-worktree-git-routing'
import {
unlockWorktreePreparation,
unlockWorktreePreparationAtPath,
verifyWorktreePreparationLock,
verifyWorktreePreparationLockAtPath,
WorktreePreparationLockOwnershipError
} from './worktree-preparation-lock'
import { addLockedWorktreePreparation } from './worktree-preparation-add'
export async function prepareWorktreeCreateCheckout(
repoPath: string,
worktreePath: string,
baseBranch: string,
lockReason: string,
options: GitWorktreeExecOptions = {},
beforeMaterialization?: Promise<void>
): Promise<void> {
// Observe early rejection while registration runs; awaiting still reports the original error.
void beforeMaterialization?.catch(() => {})
try {
await withRepoRefMaintenancePaused('worktree-prepare', () =>
runWithGitReadCacheInvalidation(async () => {
const effectiveBase = await resolveWorktreeAddBaseRef(baseBranch, (qualifiedRef) =>
hasWorktreeBaseCommitRef(repoPath, qualifiedRef, options)
)
let lockPath: string | undefined
try {
lockPath = await addLockedWorktreePreparation(
repoPath,
worktreePath,
effectiveBase,
lockReason,
{
...options,
timeout: resolveWorktreeAddTimeoutMs()
}
)
await verifyWorktreePreparationLockAtPath(lockPath, lockReason, options.signal)
let materializationBase = effectiveBase
if (beforeMaterialization) {
await waitForPromiseWithSignal(beforeMaterialization, options.signal)
await verifyWorktreePreparationLockAtPath(lockPath, lockReason, options.signal)
const { stdout } = await gitExecFileAsync(
['rev-parse', '--verify', `${effectiveBase}^{commit}`],
gitExecOptions(repoPath, options)
)
materializationBase = stdout.trim()
await verifyWorktreePreparationLockAtPath(lockPath, lockReason, options.signal)
}
// Materialize with reset to defer user post-checkout hooks until submission.
await gitExecFileAsync(
[...windowsLongPathGitArgs(worktreePath), 'reset', '--hard', materializationBase],
{ ...gitExecOptions(worktreePath, options), timeout: resolveWorktreeAddTimeoutMs() }
)
await verifyWorktreePreparationLockAtPath(lockPath, lockReason, options.signal)
} catch (error) {
if (lockPath !== undefined && !(error instanceof WorktreePreparationLockOwnershipError)) {
await performDiscardPreparedWorktree(repoPath, worktreePath, options, lockReason).catch(
() => {}
)
}
throw error
}
})
)
} finally {
notifyPreparedWorktreeMutation(repoPath)
}
}
export async function discardPreparedWorktree(
repoPath: string,
worktreePath: string,
options: GitWorktreeExecOptions = {},
expectedLockReason: string
): Promise<void> {
try {
await runWithGitReadCacheInvalidation(() =>
performDiscardPreparedWorktree(repoPath, worktreePath, options, expectedLockReason)
)
} finally {
notifyPreparedWorktreeMutation(repoPath)
}
}
export async function unlockPreparedWorktree(
repoPath: string,
worktreePath: string,
options: GitWorktreeExecOptions = {},
expectedLockReason: string
): Promise<void> {
const cleanupGitOptions = {
...gitCleanupOptions(repoPath, options),
timeout: options.timeout ?? WORKTREE_REMOVAL_REGISTRATION_TIMEOUT_MS
}
try {
await runWithGitReadCacheInvalidation(async () => {
await unlockWorktreePreparation(worktreePath, expectedLockReason, cleanupGitOptions)
})
} finally {
notifyPreparedWorktreeMutation(repoPath)
}
}
export type FinalizedPreparedWorktree = AddWorktreeResult & {
preparedHeadReset: boolean
}
export async function finalizePreparedWorktree(
repoPath: string,
preparedPath: string,
worktreePath: string,
branch: string,
baseBranch: string,
refreshLocalBaseRef = false,
options: AddWorktreeOptions = {},
expectedLockReason: string
): Promise<FinalizedPreparedWorktree> {
const finalizeGitOptions: AddWorktreeOptions = {
...options,
timeout: options.timeout ?? resolveWorktreeAddTimeoutMs()
}
try {
return await runWithGitReadCacheInvalidation(async () => {
const lockPath = await verifyWorktreePreparationLock(
preparedPath,
expectedLockReason,
finalizeGitOptions
)
const verifyOwnership = (): Promise<void> =>
verifyWorktreePreparationLockAtPath(lockPath, expectedLockReason, finalizeGitOptions.signal)
const [targetResult, preparedResult] = await Promise.allSettled([
(async () => {
const baseContext = await resolveWorktreeAddBaseContext(
repoPath,
baseBranch,
refreshLocalBaseRef,
finalizeGitOptions,
branch
)
const targetHead =
baseContext.effectiveBaseOid ??
(
await gitExecFileAsync(
['rev-parse', '--verify', `${baseContext.effectiveBase}^{commit}`],
gitExecOptions(repoPath, finalizeGitOptions)
)
).stdout.trim()
return { baseContext, targetHead }
})(),
gitExecFileAsync(
['rev-parse', '--verify', 'HEAD'],
gitExecOptions(preparedPath, finalizeGitOptions)
)
])
// Settle both reads before failure cleanup can remove the prepared checkout.
if (targetResult.status === 'rejected') {
throw targetResult.reason
}
const { baseContext, targetHead } = targetResult.value
if (preparedResult.status === 'rejected') {
await baseContext.pendingLocalBaseRefRefresh
throw preparedResult.reason
}
const preparedHeadReset = preparedResult.value.stdout.trim() !== targetHead
if (preparedHeadReset) {
await verifyOwnership()
await gitExecFileAsync(
[...windowsLongPathGitArgs(preparedPath), 'reset', '--hard', targetHead],
gitExecOptions(preparedPath, finalizeGitOptions)
)
}
let moved = false
try {
try {
await verifyOwnership()
// Git >= 2.25 needs two force flags to move a locked preparation without losing its lock reason.
await gitExecFileAsync(
[
...windowsLongPathGitArgs(repoPath),
'worktree',
'move',
'-f',
'-f',
preparedPath,
worktreePath
],
gitExecOptions(repoPath, finalizeGitOptions)
)
moved = true
} finally {
// The move rewrites both `.git` markers, and a failure can have rewritten one.
invalidateWslLinkedWorktreeGitRouting(preparedPath)
invalidateWslLinkedWorktreeGitRouting(worktreePath)
}
await verifyOwnership()
await gitExecFileAsync(
[
...windowsLongPathGitArgs(worktreePath),
'checkout',
'--no-track',
'-b',
branch,
targetHead
],
gitExecOptions(worktreePath, finalizeGitOptions)
)
await verifyOwnership()
await persistWorktreeCreationBase(
worktreePath,
branch,
baseContext.effectiveBase,
finalizeGitOptions
)
await trackSameNameRemoteBase(
worktreePath,
branch,
baseBranch,
baseContext.effectiveBase,
finalizeGitOptions
)
await configurePushAutoSetupRemote(worktreePath, finalizeGitOptions)
await unlockWorktreePreparationAtPath(
lockPath,
expectedLockReason,
finalizeGitOptions.signal
)
} catch (error) {
if (!(error instanceof WorktreePreparationLockOwnershipError)) {
await removeFailedFinalization(
repoPath,
moved ? worktreePath : preparedPath,
branch,
moved,
finalizeGitOptions,
expectedLockReason
)
}
await baseContext.pendingLocalBaseRefRefresh
throw error
}
// Advisory refresh may settle after finalization; it does not determine checkout contents.
const localBaseRefRefresh = await baseContext.pendingLocalBaseRefRefresh
return {
preparedHeadReset,
...(localBaseRefRefresh ? { localBaseRefRefresh } : {}),
...(baseContext.localBaseRefUpdateSuggestion
? { localBaseRefUpdateSuggestion: baseContext.localBaseRefUpdateSuggestion }
: {})
}
})
} finally {
notifyPreparedWorktreeMutation(repoPath)
}
}