openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
398 lines (397 loc) • 15.9 kB
JavaScript
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { a as readGitMetadataPrefix, l as resolveGitRefsBase, n as readGitHead, s as readGitRefs } from "./git-root-ChTibfp1.mjs";
import { f as requireGitCommandOutput, n as GitCommandTimeoutError, r as createGitCommandError } from "./git-exec-BOEpfMeZ.mjs";
import { t as canReadGitFilesystemRefs } from "./git-worker-context-DDPUBONa.mjs";
import { d as runGit } from "./git-D3obqZyl.mjs";
import { t as parseGitHubRemoteUrl } from "./github-remote-rTdEnJZN.mjs";
import fs from "node:fs";
import path from "node:path";
import { readRegularFile } from "@openclaw/fs-safe/advanced";
//#region src/gateway/control-ui-session-prs-landing.ts
/** Only ordinary file-backed refs bypass Git; unusual discovery/layouts retain its semantics. */
function readCheckoutHead(root) {
if (!canReadGitFilesystemRefs()) return null;
try {
const head = readGitHead(root, { maxDepth: 1 });
if (!head?.value || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(head.value) || fs.lstatSync(head.headPath).isSymbolicLink()) return null;
const sha = head.value.toLowerCase();
const refsBase = head.refsBase ?? resolveGitRefsBase(head.headPath);
if (fs.existsSync(path.join(refsBase, "reftable")) || head.ref !== null && !head.ref.startsWith("refs/heads/")) return null;
const branch = head.ref?.slice(11) ?? null;
if (branch !== null && /^(?:[A-Z_]+$|(?:refs|bisect|worktree|rewritten|main-worktree|worktrees)\/)/.test(branch)) return null;
const headAliases = [
"refs/HEAD",
"refs/tags/HEAD",
"refs/heads/HEAD",
"refs/remotes/HEAD",
"refs/remotes/HEAD/HEAD"
];
const branchAliases = branch === null ? [] : [
`refs/${branch}`,
`refs/tags/${branch}`,
`refs/remotes/${branch}`,
`refs/remotes/${branch}/HEAD`
];
const aliases = readGitRefs(refsBase, [...headAliases, ...branchAliases]);
if (headAliases.some((ref) => aliases.get(ref) !== null)) return null;
if (branch === null) return {
sha,
branch,
refsBase
};
return {
sha,
refsBase,
...fs.existsSync(path.join(refsBase, branch)) || branchAliases.some((ref) => aliases.get(ref) !== null) ? {} : { branch }
};
} catch {
return null;
}
}
/** Background PR reads never hydrate objects; old Git also has no permitted transport. */
async function runPullRequestGit(cwd, args, options = {}) {
const result = await runGit(cwd, args, {
...options,
env: {
GIT_NO_LAZY_FETCH: "1",
GIT_ALLOW_PROTOCOL: ""
}
});
if (result.termination === "timeout") throw createGitCommandError(`git ${args[0]}`, result);
return result;
}
async function gitOutput(cwd, args) {
const result = await runPullRequestGit(cwd, args);
return result.code === 0 ? result.stdout.trim() || null : null;
}
async function readRemoteRevisions(root, refs, head) {
if (head) try {
const values = readGitRefs(head.refsBase, refs);
if (refs.every((ref) => {
const value = values.get(ref);
return !ref.split("/").includes("..") && !fs.lstatSync(path.join(head.refsBase, ref), { throwIfNoEntry: false })?.isSymbolicLink() && (value === null || typeof value === "string" && value.length === head.sha.length && /^[a-f0-9]+$/i.test(value));
})) return new Map([...values].flatMap(([ref, value]) => value ? [[ref, value.toLowerCase()]] : []));
} catch {}
try {
const result = await runPullRequestGit(root, [
"for-each-ref",
"--format=%(refname)%00%(objectname)",
"--",
...refs
], {
maxOutputBytes: 16384,
terminateOnOutputLimit: true
});
const output = requireGitCommandOutput("git for-each-ref", result);
const revisions = /* @__PURE__ */ new Map();
for (const line of output.trim().split("\n")) {
const separator = line.indexOf("\0");
const ref = line.slice(0, separator);
if (separator > 0 && refs.includes(ref)) revisions.set(ref, line.slice(separator + 1));
}
return revisions;
} catch (error) {
if (error instanceof GitCommandTimeoutError) throw error;
const revisions = /* @__PURE__ */ new Map();
for (const ref of refs) {
const revision = await gitOutput(root, [
"rev-parse",
"--verify",
"--quiet",
ref
]);
if (revision) revisions.set(ref, revision);
}
return revisions;
}
}
async function isAncestor(root, ancestor, descendant) {
return (await runPullRequestGit(root, [
"merge-base",
"--is-ancestor",
ancestor,
descendant
])).code === 0;
}
/** Prefer a maximal published baseline containing the merge base, then input order. */
async function maximalCommit(root, candidates) {
const unique = [...new Set(candidates)];
const first = unique[0];
if (first === void 0 || unique.length === 1) return first ?? null;
const second = unique[1];
if (unique.length === 2 && second !== void 0) return await isAncestor(root, first, second) ? second : first;
const out = await gitOutput(root, [
"merge-base",
"--independent",
...unique
]);
if (!out) return first;
const independent = new Set(out.split("\n").map((line) => line.trim()).filter(Boolean));
const maxima = unique.filter((candidate) => independent.has(candidate));
for (const candidate of maxima) if (candidate === first || maxima.length === 1 || await isAncestor(root, first, candidate)) return candidate;
return maxima[0] ?? first;
}
async function resolveBranchLanding(root, params) {
const pushedRef = `refs/remotes/origin/${params.branch}`;
const defaultRef = params.defaultBranch ? `refs/remotes/origin/${params.defaultBranch}` : null;
const checkoutHead = readCheckoutHead(root);
const revisions = await readRemoteRevisions(root, [pushedRef, ...defaultRef ? [defaultRef] : []], checkoutHead);
const pushedSha = revisions.get(pushedRef) ?? null;
const headSha = checkoutHead?.sha ?? await gitOutput(root, [
"rev-parse",
"--verify",
"--quiet",
"HEAD"
]);
const defaultSha = defaultRef ? revisions.get(defaultRef) ?? null : null;
const possibleLandings = params.mergedHeads.filter((head) => head.baseRef === params.defaultBranch || Boolean(params.defaultBranch && head.mergeCommitSha));
const landedHeads = [];
for (const head of possibleLandings) if (head.baseRef === params.defaultBranch) landedHeads.push(head);
else if (defaultSha && head.mergeCommitSha && await isAncestor(root, head.mergeCommitSha, defaultSha)) landedHeads.push(head);
const landedShas = new Set(landedHeads.map((head) => head.sha));
const mergeBase = defaultSha && headSha ? defaultSha === headSha ? headSha : await gitOutput(root, [
"merge-base",
defaultSha,
headSha
]) : null;
const baselines = mergeBase ? [mergeBase] : [];
if (headSha) {
for (const merged of landedShas) if (await isAncestor(root, merged, headSha)) baselines.push(merged);
else if (await isAncestor(root, headSha, merged)) baselines.push(headSha);
}
const statsBase = await maximalCommit(root, baselines);
let provenNewPushedWork = false;
if (pushedSha && mergeBase && !landedShas.has(pushedSha.toLowerCase())) {
provenNewPushedWork = landedHeads.length > 0;
for (const head of landedHeads) if (!((head.mergeCommitSha ? await isAncestor(root, head.mergeCommitSha, mergeBase) : false) || await isAncestor(root, head.sha, mergeBase))) {
provenNewPushedWork = false;
break;
}
}
return {
pushedSha,
defaultSha,
statsBase,
hasLandedPullRequest: landedHeads.length > 0,
provenNewPushedWork
};
}
//#endregion
//#region src/gateway/control-ui-session-prs-git.runtime.ts
const log = createSubsystemLogger("git/branch-facts");
/** Observe only this checkout's refs; snapshot publication does not change PR facts. */
async function readCheckoutGitRevision({ root, includeIndex, branch, defaultBranch }) {
if (!canReadGitFilesystemRefs()) return null;
try {
const head = readGitHead(root, { maxDepth: 1 });
const checkout = readCheckoutHead(root);
const defaultRef = readDefaultRef(checkout);
if (!head || !checkout || checkout.branch === void 0 || !includeIndex && defaultRef === void 0) return null;
const gitDir = path.dirname(head.headPath);
const paths = [
path.join(root, ".git"),
path.join(checkout.refsBase, "config"),
path.join(gitDir, "config.worktree")
];
const replacements = path.join(checkout.refsBase, "refs/replace");
let packedReplacements = [];
if (includeIndex) {
paths.push(path.join(gitDir, "index"), path.join(checkout.refsBase, "shallow"), path.join(checkout.refsBase, "info/grafts"));
if (fs.existsSync(replacements)) paths.push(...fs.readdirSync(replacements, {
recursive: true,
encoding: "utf8"
}).map((name) => path.join(replacements, name)));
const packed = path.join(checkout.refsBase, "packed-refs");
if (fs.existsSync(packed)) packedReplacements = fs.readFileSync(packed, "utf8").split("\n").filter((line) => /\srefs\/replace\//u.test(line)).toSorted();
}
const selectedBranch = branch ?? checkout.branch;
const selectedDefault = defaultBranch ? `origin/${defaultBranch}` : defaultRef;
const tips = await readRemoteRevisions(root, [...selectedBranch ? [`refs/remotes/origin/${selectedBranch}`] : [], ...selectedDefault ? [`refs/remotes/${selectedDefault}`] : []], checkout);
return JSON.stringify([
checkout,
defaultRef,
[...tips],
packedReplacements,
paths.toSorted().map((file) => {
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
if (stat?.isSymbolicLink() || stat?.isFile() && file.startsWith(`${replacements}${path.sep}`) && readGitMetadataPrefix(file).startsWith("ref:")) throw new Error("Symbolic Git metadata requires Git discovery");
return stat?.isDirectory() ? [
file,
stat.dev,
stat.ino
] : [
file,
stat?.ino,
stat?.size,
stat?.mtimeMs,
stat?.ctimeMs
];
})
]);
} catch {
return null;
}
}
function readDefaultRef(head) {
if (!head) return;
try {
const ref = "refs/remotes/origin/HEAD";
if (fs.lstatSync(path.join(head.refsBase, ref), { throwIfNoEntry: false })?.isSymbolicLink()) return;
const raw = readGitRefs(head.refsBase, [ref]).get(ref);
if (raw === null) return null;
const match = /^ref:\s+(refs\/remotes\/(origin\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*))$/u.exec(raw ?? "");
if (!match) return;
const target = match[1];
const short = match[2];
const aliases = [
`refs/${short}`,
`refs/tags/${short}`,
`refs/heads/${short}`,
`refs/remotes/${short}/HEAD`
];
const values = readGitRefs(head.refsBase, [target, ...aliases]);
const value = values.get(target);
return (value === null || /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value ?? "")) && !fs.lstatSync(path.join(head.refsBase, target), { throwIfNoEntry: false })?.isSymbolicLink() && !fs.existsSync(path.join(head.refsBase, short)) && aliases.every((alias) => values.get(alias) === null) ? short : void 0;
} catch {
return;
}
}
async function readCheckoutGitContext(root, githubHost = "github.com") {
const head = readCheckoutHead(root);
const branch = head?.branch === null ? "HEAD" : head?.branch ?? await gitOutput(root, [
"rev-parse",
"--abbrev-ref",
"HEAD"
]);
if (!branch) return null;
const remoteUrl = await gitOutput(root, [
"remote",
"get-url",
"origin"
]);
const publicRemote = remoteUrl ? parseGitHubRemoteUrl(remoteUrl) : null;
const remote = publicRemote ?? (remoteUrl ? parseGitHubRemoteUrl(remoteUrl, githubHost) : null);
if (!remote) return null;
const preparedDefaultRef = readDefaultRef(head);
const defaultBranch = (preparedDefaultRef === void 0 ? await gitOutput(root, [
"symbolic-ref",
"--short",
"refs/remotes/origin/HEAD"
]) : preparedDefaultRef)?.replace(/^origin\//, "");
return {
...remote,
...!publicRemote ? { host: githubHost } : {},
branch: branch === "HEAD" ? null : branch,
root,
...defaultBranch ? { defaultBranch } : {}
};
}
const SHORTSTAT_FILES = /(\d+) files? changed/;
const SHORTSTAT_INSERTIONS = /(\d+) insertion/;
const SHORTSTAT_DELETIONS = /(\d+) deletion/;
const MAX_UNTRACKED_STAT_FILES = 100;
const MAX_UNTRACKED_STAT_BYTES = 524288;
/** Count lines without a subprocess per file; hardlinked content only exposes a count. */
async function untrackedFileAdditions(root, filePath) {
try {
const { buffer: body } = await readRegularFile({
filePath: path.resolve(root, filePath),
maxBytes: MAX_UNTRACKED_STAT_BYTES
});
if (body.length === 0 || body.subarray(0, 8192).includes(0)) return 0;
let lines = 0;
for (const byte of body) if (byte === 10) lines += 1;
return body[body.length - 1] === 10 ? lines : lines + 1;
} catch {
return 0;
}
}
async function untrackedStats(root) {
const listing = await runPullRequestGit(root, [
"ls-files",
"--others",
"--exclude-standard",
"-z"
]);
const paths = listing?.code === 0 ? listing.stdout.split("\0").filter(Boolean) : [];
let additions = 0;
for (const filePath of paths.slice(0, MAX_UNTRACKED_STAT_FILES)) additions += await untrackedFileAdditions(root, filePath);
return {
additions,
files: paths.length
};
}
/**
* Working-tree diff counts vs an explicit base, untracked files included:
* the size the PR would have if the current work were committed and pushed;
* changedFiles decides row visibility for unpushed branches. Unlike bare
* `git diff`, this also counts unmerged (conflict) paths.
*/
async function diffStatsAgainst(root, base, refreshIndex) {
try {
const inventory = await runPullRequestGit(root, [
"-c",
"diff.autoRefreshIndex=false",
"diff",
"--raw",
"--no-abbrev",
"--no-renames",
"--no-ext-diff",
"--no-textconv",
"-z",
base,
"--"
]);
const objects = /* @__PURE__ */ new Set();
const fields = requireGitCommandOutput("git diff --raw", inventory).split("\0");
for (let i = 0; i < fields.length - 1; i += 2) {
const [oldMode, newMode, oldObject, newObject] = fields[i].slice(1).split(" ");
for (const [mode, object] of [[oldMode, oldObject], [newMode, newObject]]) if (mode !== "160000" && object && !/^0+$/u.test(object)) objects.add(object);
}
if (objects.size > 0) {
const input = `${[...objects].join("\n")}\n`;
const available = await runPullRequestGit(root, ["cat-file", "--batch-check=%(objectname)"], { input });
if (requireGitCommandOutput("git cat-file", available) !== input) return null;
}
const result = await runPullRequestGit(root, [
"-c",
`diff.autoRefreshIndex=${refreshIndex}`,
"diff",
"--shortstat",
"--no-ext-diff",
"--no-textconv",
base
]);
if (result.code !== 0) return null;
const summary = result.stdout.trim();
const untracked = await untrackedStats(root);
return {
additions: Number(SHORTSTAT_INSERTIONS.exec(summary)?.[1] ?? 0) + untracked.additions,
deletions: Number(SHORTSTAT_DELETIONS.exec(summary)?.[1] ?? 0),
changedFiles: Number(SHORTSTAT_FILES.exec(summary)?.[1] ?? 0) + untracked.files
};
} catch (error) {
if (error instanceof GitCommandTimeoutError) throw error;
return null;
}
}
async function readPullRequestBranchFacts(input) {
try {
const landing = await resolveBranchLanding(input.root, input);
const stats = landing.statsBase ? await diffStatsAgainst(input.root, landing.statsBase, input.refreshIndex === true) : null;
const noPushedChanges = stats !== null && landing.defaultSha !== null && landing.defaultSha === landing.pushedSha && landing.statsBase === landing.defaultSha;
const creatable = (!landing.hasLandedPullRequest || landing.provenNewPushedWork) && !noPushedChanges && Boolean(input.defaultBranch) && landing.pushedSha !== null && (!landing.defaultSha || !await isAncestor(input.root, landing.pushedSha, landing.defaultSha));
return !creatable && !(stats && stats.changedFiles > 0) ? void 0 : {
creatable,
stats
};
} catch {
log.warn("PR comparison unavailable; fetch repository history and retry. Dependent comparisons skipped.");
return {
creatable: false,
stats: null
};
}
}
//#endregion
export { readCheckoutGitContext, readCheckoutGitRevision, readPullRequestBranchFacts };