openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
375 lines (374 loc) • 14.9 kB
JavaScript
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 };