UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

375 lines (374 loc) • 14.9 kB
import { r as resolveGlobalMap } from "./global-singleton-0m-JziJW.mjs"; import { _ as redactSensitiveText } from "./redact-BV3wTtTv.mjs"; import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { o as readCommandProcessFailure, t as CommandProcessCleanupError } from "./exec-result-DXhDHhYI.mjs"; import { s as hasWorktreeUnknownOutcome } from "./errors-CgO78Orl.mjs"; import { f as requireGitCommandOutput, r as createGitCommandError, u as normalizeGitPathForFilesystem } from "./git-exec-BOEpfMeZ.mjs"; import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.mjs"; import { d as runGit, o as listGitWorktrees, s as requireGit } from "./git-D3obqZyl.mjs"; import { p as estimateWorktreeGitBytes } from "./allocation-li13b6uz.mjs"; import { t as withWorktreeGitConfig } from "./checkout-git-config-D9Pjt5zi.mjs"; import path from "node:path"; import fs from "node:fs/promises"; import { randomUUID } from "node:crypto"; //#region src/agents/worktrees/base-ref.ts const log = createSubsystemLogger("agents/worktrees"); const remoteDefaults = resolveGlobalMap(Symbol.for("openclaw.worktreeRemoteDefaults")); var InvalidWorktreeBaseRefError = class extends Error { constructor(options) { super("Worktree base ref does not resolve to a commit. Choose a local or remote branch and retry.", options); this.name = "InvalidWorktreeBaseRefError"; } }; async function resolveWorktreeCreationBase(repoRoot, params) { if (params.checkoutCommit) { if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(params.checkoutCommit)) throw new Error("Worktree checkout commit is invalid"); return { commit: params.checkoutCommit, gitOperand: params.checkoutCommit, recordRef: params.baseRef ?? params.checkoutCommit, fetchSucceeded: void 0, warning: void 0 }; } return params.prepareBase && !params.baseRef ? await params.prepareBase({ signal: params.signal, assertCurrent: params.commitGuard, localDefault: "fast-forward" }) : await resolveWorktreeBase(repoRoot, params.baseRef, params.signal, params.commitGuard, "fast-forward"); } async function resolveWorktreeBase(repoRoot, baseRef, signal, assertCurrent, localDefault = "preserve") { if (baseRef) { const verified = await runGit(repoRoot, [ "-c", "core.warnAmbiguousRefs=true", "rev-parse", "--verify", "--end-of-options", `${baseRef === "-" ? "@{-1}" : baseRef}^{commit}` ], { signal, beforeRun: assertCurrent }); signal?.throwIfAborted(); if (verified.termination === "exit" && typeof verified.code === "number" && verified.code !== 0) throw new InvalidWorktreeBaseRefError({ cause: createGitCommandError("git rev-parse --verify", verified) }); const commit = requireGitCommandOutput("git rev-parse --verify", verified).trim(); if (!commit || commit.includes("\n") || verified.stderr.trim()) throw new InvalidWorktreeBaseRefError({ cause: createGitCommandError("git rev-parse --verify", verified) }); return { commit, gitOperand: baseRef !== "-" && baseRef.startsWith("-") ? commit : baseRef, recordRef: baseRef }; } const options = { signal, beforeRun: () => { signal?.throwIfAborted(); assertCurrent?.(); }, env: { GIT_NO_LAZY_FETCH: "1", GIT_TERMINAL_PROMPT: "0" } }; const commonDir = await fs.realpath(path.resolve(repoRoot, normalizeGitPathForFilesystem(await requireGit(repoRoot, ["rev-parse", "--git-common-dir"], options)))); return await withWorktreeBasePreparation({ repoRoot: await fs.realpath(repoRoot), commonDir }, (resolve) => resolve({ signal, assertCurrent, localDefault })); } /** Capture a creation cohort before capacity waits, without fetching until a source owner consumes it. */ async function withWorktreeBasePreparation(repository, run) { const key = JSON.stringify([repository.commonDir, repository.repoRoot].map((directory) => process.platform === "win32" ? directory.toLowerCase() : directory)); const shared = remoteDefaults.get(key) ?? { borrowers: 0 }; shared.borrowers++; remoteDefaults.set(key, shared); const forget = () => { if (remoteDefaults.get(key) === shared) remoteDefaults.delete(key); }; let active = true; const resolutions = []; const resolve = async ({ signal, assertCurrent, localDefault = "preserve" }) => { const options = { signal, beforeRun: () => { if (!active) throw new Error("Worktree base preparation is closed"); signal?.throwIfAborted(); assertCurrent?.(); }, env: { GIT_NO_LAZY_FETCH: "1", GIT_TERMINAL_PROMPT: "0" } }; for (;;) { options.beforeRun(); let attempt = shared.attempt; if (!attempt) { const started = { ownerInvalidated: false, pending: fetchRemoteDefault(repository.repoRoot, options).then(async (base) => { const preparationKey = randomUUID(); await estimateWorktreeGitBytes(repository.repoRoot, base.commit, { signal, assertCurrent: options.beforeRun, preparationKey }); base.preparationKey = preparationKey; options.beforeRun(); return base; }).catch((error) => { if (hasWorktreeUnknownOutcome(error)) throw error; if (readCommandProcessFailure(error)?.cleanup === "uncertain") throw new CommandProcessCleanupError({ cause: error }); try { options.beforeRun(); } catch { started.ownerInvalidated = true; } throw error; }).finally(forget) }; shared.attempt = attempt = started; } let selected; try { selected = await attempt.pending; } catch (error) { if (hasWorktreeUnknownOutcome(error)) throw error; options.beforeRun(); if (attempt.ownerInvalidated) { if (shared.attempt === attempt) shared.attempt = void 0; continue; } throw error; } options.beforeRun(); const { branch, ...base } = selected; const warning = localDefault === "fast-forward" ? await fastForwardLocalDefault(repository.repoRoot, branch, base.commit, options) : void 0; return warning ? { ...base, warning: [base.warning, redactSensitiveText(warning)].filter(Boolean).join("\n") } : base; } }; try { return await run((options) => { const pending = resolve(options); resolutions.push(pending); return pending; }); } finally { active = false; if (--shared.borrowers === 0 && !shared.attempt) forget(); await Promise.allSettled(resolutions); } } async function fetchRemoteDefault(repoRoot, options) { const cached = await runGit(repoRoot, [ "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD" ], options); const advertised = await runGit(repoRoot, [ "ls-remote", "--symref", "origin", "HEAD" ], { ...options, timeoutMs: 3e4 }); options.signal?.throwIfAborted(); const advertisedOk = advertised.termination === "exit" && advertised.code === 0; const advertisedBranch = advertisedOk ? /^ref: refs\/heads\/(.+)\tHEAD\r?$/mu.exec(advertised.stdout)?.[1] : void 0; const cachedBranch = cached.termination === "exit" && cached.code === 0 ? /^refs\/remotes\/origin\/(.+)$/u.exec(cached.stdout.trim())?.[1] : void 0; let branch = advertisedBranch ?? cachedBranch; const warnings = []; if (!advertisedOk) warnings.push(createGitCommandError("git ls-remote origin HEAD", advertised).message); else if (!advertisedBranch) warnings.push("origin did not advertise a default branch; using its cached default."); if (!branch || branch === "HEAD") throw new Error(`Remote default branch is unavailable. Repair origin or choose an explicit worktree base ref. ${redactSensitiveText(warnings.join("\n"))}`); let remoteRef = `refs/remotes/origin/${branch}`; const fetched = await runGit(repoRoot, [ "fetch", "--no-auto-maintenance", "--no-recurse-submodules", "--no-tags", "origin", `+refs/heads/${branch}:${remoteRef}` ], { ...options, timeoutMs: 6e4 }); if (fetched.cleanup === "uncertain") throw new CommandProcessCleanupError(); options.signal?.throwIfAborted(); const fetchSucceeded = fetched.termination === "exit" && fetched.code === 0; if (!fetchSucceeded) { warnings.push(createGitCommandError("git fetch origin default branch", fetched).message); branch = cachedBranch ?? branch; remoteRef = `refs/remotes/origin/${branch}`; } const verified = await runGit(repoRoot, [ "rev-parse", "--verify", `${remoteRef}^{commit}` ], options); if (verified.termination !== "exit" || verified.code !== 0) throw new Error(`Remote default ${remoteRef} is unavailable. Repair origin or choose an explicit worktree base ref. ${redactSensitiveText(warnings.join("\n"))}`); const commit = requireGitCommandOutput("git rev-parse remote default", verified).trim(); if (fetchSucceeded && advertisedBranch && cachedBranch !== branch) await requireGit(repoRoot, [ "symbolic-ref", "refs/remotes/origin/HEAD", remoteRef ], options); return { branch, commit, gitOperand: remoteRef, recordRef: `origin/${branch}`, fetchSucceeded, ...warnings.length ? { warning: redactSensitiveText(warnings.join("\n")) } : {} }; } var LocalDefaultBusyError = class extends Error {}; async function assertWorktreeGitOperationsIdle(repoRoot, localRef, options) { const commonDir = path.resolve(repoRoot, normalizeGitPathForFilesystem(await requireGit(repoRoot, ["rev-parse", "--git-common-dir"], options))); try { const worktreesDir = path.join(commonDir, "worktrees"); const worktrees = await fs.readdir(worktreesDir).catch((error) => { if (hasErrnoCode(error, "ENOENT")) return []; throw error; }); const busy = () => new LocalDefaultBusyError("Local default retained: finish or abort the Git rebase, am, or bisect operation reserving it before advancing it."); if ((await fs.readdir(commonDir)).some((name) => [ "rebase-merge", "rebase-apply", "BISECT_LOG" ].includes(name))) throw busy(); const read = async (directory, name) => await fs.readFile(path.join(directory, name), "utf8").catch((error) => { if (hasErrnoCode(error, "ENOENT")) return; throw error; }); const checked = await runTasksWithConcurrency({ limit: 16, errorMode: "stop", tasks: worktrees.map((name) => async () => { options.signal?.throwIfAborted(); options.beforeRun?.(); const directory = path.join(worktreesDir, name); for (const file of [ "rebase-merge/head-name", "rebase-apply/head-name", "rebase-merge/update-refs" ]) if ((await read(directory, file))?.trim().split("\n").includes(localRef)) throw busy(); const bisect = (await read(directory, "BISECT_START"))?.trim(); if (bisect === localRef || bisect === localRef.slice(11)) { if (await read(directory, "BISECT_LOG") !== void 0) throw busy(); } }) }); if (checked.hasError) throw checked.firstError; } catch (error) { options.signal?.throwIfAborted(); options.beforeRun?.(); if (error instanceof LocalDefaultBusyError) throw error; throw new LocalDefaultBusyError("Local default retained: Git worktree operation state could not be checked. Inspect Git worktree metadata before retrying.", { cause: error }); } } async function fastForwardLocalDefault(repoRoot, branch, commit, options) { const localRef = `refs/heads/${branch}`; const local = await runGit(repoRoot, [ "rev-parse", "--verify", localRef ], options); if (local.termination !== "exit" || local.code !== 0 || local.stdout.trim() === commit) return; const previous = local.stdout.trim(); const ancestor = await runGit(repoRoot, [ "merge-base", "--is-ancestor", previous, commit ], options); if (ancestor.termination !== "exit" || ancestor.code !== 0) return; const primary = await runGit(repoRoot, [ "symbolic-ref", "--quiet", "HEAD" ], options); if (primary.termination !== "exit" || primary.code !== 0 || primary.stdout.trim() !== localRef) return; const advanced = await withWorktreeGitConfig(repoRoot, true, options, async (git) => await git.run(repoRoot, [ "merge", "--ff-only", "--no-edit", "--no-stat", "--no-overwrite-ignore", commit ], { ...options, killProcessTree: true, startRun: async (run) => { await assertWorktreeGitOperationsIdle(repoRoot, localRef, options); const checkouts = (await listGitWorktrees(repoRoot, options)).filter((entry) => entry.branch === localRef); const checkout = checkouts[0]; if (!checkout || checkouts.length > 1 || path.resolve(checkout.path) !== path.resolve(repoRoot) || checkout.lockedReason !== void 0) throw new LocalDefaultBusyError(); const sparse = await runGit(repoRoot, [ "config", "--bool", "core.sparseCheckout" ], options); if (sparse.termination !== "exit" || sparse.code !== 0 && sparse.code !== 1 || sparse.stdout.trim() === "true") throw new LocalDefaultBusyError(); const dirty = await git.run(repoRoot, [ "status", "--porcelain", "--untracked-files=all" ], options); if (dirty.termination !== "exit" || dirty.code !== 0 || dirty.stdout.trim()) throw new LocalDefaultBusyError(); const current = await runGit(repoRoot, [ "symbolic-ref", "--quiet", "HEAD" ], options); if (current.termination !== "exit" || current.code !== 0 || current.stdout.trim() !== localRef) throw new LocalDefaultBusyError(); return await run(); } })).catch((error) => { if (error instanceof LocalDefaultBusyError) return error; throw error; }); if (advanced instanceof LocalDefaultBusyError) return advanced.message || void 0; if (advanced.termination !== "exit" || advanced.code !== 0) return createGitCommandError("git fast-forward local default", advanced).message; } /** Report the immutable commit captured by checkout registration, not an earlier ref value. */ async function logWorktreeBase(repoRoot, base, commit, context) { const committedAt = Number(await requireGit(repoRoot, [ "show", "-s", "--no-show-signature", "--no-notes", "--format=%ct", commit ], { signal: context.signal, beforeRun: context.assertCurrent, env: { GIT_NO_LAZY_FETCH: "1" } })); const ageDays = Math.max(0, (context.now / 1e3 - committedAt) / 86400); const stale = ageDays > 7; const message = `worktree base ${base.recordRef} at ${commit} (commit age ${ageDays.toFixed(1)} days; fetch ${base.fetchSucceeded === void 0 ? "not requested" : base.fetchSucceeded ? "succeeded" : "failed"})${base.warning ? `: ${base.warning}` : ""}${stale ? "; base is older than 7 days. Check origin before starting work." : ""}`; const metadata = { worktreePath: context.worktreePath, ownerId: context.ownerId, baseRef: base.recordRef, baseCommit: commit, ageDays, fetchSucceeded: base.fetchSucceeded }; if (base.warning || stale) log.warn(message, metadata); else log.info(message, metadata); } //#endregion export { withWorktreeBasePreparation as a, resolveWorktreeCreationBase as i, logWorktreeBase as n, resolveWorktreeBase as r, InvalidWorktreeBaseRefError as t };