UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,281 lines (1,280 loc) • 44.2 kB
import "./src-SskUVPJG.mjs"; import { t as expectDefined } from "./expect-lbe3Hgrh.mjs"; import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.mjs"; import { a as resolveGitRefsBase, i as readGitRefs, n as readGitHead, r as readGitMetadataPrefix } from "./git-root-Bp6q-ESK.mjs"; import { t as GIT_TIMEOUT_MS, u as requireGitCommandOutput } from "./git-exec-DPoSKq41.mjs"; import { t as _usingCtx } from "./usingCtx-DwOBYOJf.mjs"; import { c as WorktreeRepositoryError, t as canReadGitFilesystemRefs } from "./git-worker-context-BSo6KDXb.mjs"; import { d as runGit, f as runGitBuffered, o as insideGitCheckout } from "./git-DOeuOnFt.mjs"; import { t as resolveCheckoutRootFromRealPath } from "./repository-paths-DPi5Zd3r.mjs"; import { t as parseGitHubRemoteUrl } from "./github-remote-CloOx2kv.mjs"; import fs from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; import crypto from "node:crypto"; import { readFileWindowFully, readRegularFile } from "@openclaw/fs-safe/advanced"; import { root } from "@openclaw/fs-safe/root"; //#region src/agents/worktrees/branches.runtime.ts const BRANCH_INVENTORY_MAX_OUTPUT_BYTES = 262144; const BRANCH_SUGGESTIONS_PER_KIND = 100; const branchInventories = /* @__PURE__ */ new Map(); function branchInventoryRevision(repoRoot) { if (!canReadGitFilesystemRefs()) return; try { const stamps = []; const stamp = (file) => { const stat = fs.lstatSync(file, { bigint: true, throwIfNoEntry: false }); if (stat?.isSymbolicLink() || stamps.length >= 1024) throw new Error("Uncacheable ref inventory"); stamps.push(file, stat ? `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}` : "missing"); return stat; }; const marker = path.join(repoRoot, ".git"); const markerStat = stamp(marker); const pointer = markerStat?.isFile() && markerStat.size <= 4096n ? /^gitdir: (.+)\r?\n?$/.exec(readGitMetadataPrefix(marker, 4096))?.[1]?.trim() : void 0; const gitDir = markerStat?.isDirectory() ? marker : pointer ? path.resolve(repoRoot, pointer) : void 0; if (!gitDir) return; const head = path.join(gitDir, "HEAD"); const common = resolveGitRefsBase(head); if (fs.existsSync(path.join(common, "reftable"))) return; for (const file of [ head, path.join(gitDir, "commondir"), path.join(common, "packed-refs"), path.join(common, "config") ]) stamp(file); const visit = (directory, depth = 0) => { if (depth > 32 || !stamp(directory)?.isDirectory()) throw new Error("Uncacheable ref directory"); for (const name of fs.readdirSync(directory).toSorted()) { const file = path.join(directory, name); if (stamp(file)?.isDirectory()) visit(file, depth + 1); } }; visit(path.join(common, "refs")); const worktreeRefs = path.join(gitDir, "refs"); if (common !== gitDir && stamp(worktreeRefs)?.isDirectory()) visit(worktreeRefs); return JSON.stringify(stamps); } catch { return; } } async function readBranchInventory(repoRoot) { const revision = branchInventoryRevision(repoRoot); const cached = branchInventories.get(repoRoot); branchInventories.delete(repoRoot); if (revision !== void 0 && cached?.revision === revision) { branchInventories.set(repoRoot, cached); return cached.refs; } const refs = await listRepositoryBranchRefs(repoRoot, ["refs/heads/", "refs/remotes/"]); if (revision !== void 0 && revision === branchInventoryRevision(repoRoot)) { branchInventories.set(repoRoot, { revision, refs }); pruneMapToMaxSize(branchInventories, 64); } return refs; } async function listRepositoryBranchRefs(repoRoot, patterns, count) { const result = await runGit(repoRoot, [ "-c", "core.warnAmbiguousRefs=true", "for-each-ref", ...count === void 0 ? [] : [`--count=${count}`], "--sort=refname", count === void 0 ? "--format=%(refname)%00%(refname:short)%00%(symref)%00%(HEAD)" : "--format=%(refname)%00%(refname:short)", ...patterns ], { maxOutputBytes: BRANCH_INVENTORY_MAX_OUTPUT_BYTES, terminateOnOutputLimit: count === void 0 }); const output = requireGitCommandOutput("git for-each-ref", result); const branches = []; for (const line of output.trim().split("\n")) { const [ref, name, symbolicRef, head] = line.split("\0"); const metadata = { symbolicRef: symbolicRef || void 0, current: head === "*" }; if (!ref || !name) continue; if (ref.startsWith("refs/heads/")) branches.push({ ref, ...metadata, branchName: ref.slice(11), branch: { name, kind: "local" } }); else if (ref.startsWith("refs/remotes/")) { const remoteRef = ref.slice(13); const slash = remoteRef.indexOf("/"); const branchName = slash > 0 ? remoteRef.slice(slash + 1) : ""; branches.push({ ref, branchName, ...metadata, branch: { name, kind: "remote" } }); } } return branches; } async function readRepositoryBranches(repoRoot, options = {}) { let sourceRoot; try { const requested = await fs$1.realpath(repoRoot).catch(() => { throw new Error(`repository does not exist: ${repoRoot}`); }); if (options.includeRepositoryStatus) { if (!(await fs$1.stat(requested)).isDirectory()) return { branches: [], repositoryStatus: "unavailable" }; if (!insideGitCheckout(requested)) return { branches: [], repositoryStatus: "not_git" }; } sourceRoot = await resolveCheckoutRootFromRealPath(requested, repoRoot); } catch (error) { if (options.includeRepositoryStatus) { if (error instanceof WorktreeRepositoryError && error.reason === "unborn") return { branches: [], repositoryStatus: "not_git" }; return { branches: [], repositoryStatus: "unavailable" }; } throw error; } let inventory; try { inventory = await readBranchInventory(sourceRoot); } catch { inventory = void 0; } const branches = /* @__PURE__ */ new Map(); let branchesUnavailable = false; for (const prefix of ["refs/remotes/", "refs/heads/"]) try { const entries = inventory ? inventory.filter((entry) => entry.ref.startsWith(prefix)).slice(0, BRANCH_SUGGESTIONS_PER_KIND) : await listRepositoryBranchRefs(sourceRoot, [prefix], BRANCH_SUGGESTIONS_PER_KIND); for (const entry of entries) if (entry.branchName && (entry.branch.kind !== "remote" || entry.branchName !== "HEAD")) branches.set(entry.branchName, entry); } catch { branchesUnavailable = true; } let defaultRef; let headRef; if (inventory) { defaultRef = inventory.find((entry) => entry.ref === "refs/remotes/origin/HEAD")?.symbolicRef; headRef = inventory.find((entry) => entry.current)?.ref; } else { const remoteHead = await runGit(sourceRoot, [ "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD" ]); defaultRef = remoteHead.code === 0 ? remoteHead.stdout.trim() : void 0; const head = await runGit(sourceRoot, [ "symbolic-ref", "--quiet", "HEAD" ]); headRef = head.code === 0 ? head.stdout.trim() : void 0; } const resolveBranch = async (ref) => { if (!ref) return; const known = (inventory ?? [...branches.values()]).find((entry) => entry.ref === ref); if (known || inventory) return known?.branchName && (known.branch.kind !== "remote" || known.branchName !== "HEAD") ? known : void 0; try { return (await listRepositoryBranchRefs(sourceRoot, [ref], 1)).find((entry) => entry.ref === ref && entry.branchName && (entry.branch.kind !== "remote" || entry.branchName !== "HEAD")); } catch { branchesUnavailable = true; return; } }; const defaultEntry = await resolveBranch(defaultRef?.startsWith("refs/remotes/origin/") ? `refs/heads/${defaultRef.slice(20)}` : void 0) ?? await resolveBranch(defaultRef); const headEntry = await resolveBranch(headRef); for (const entry of [defaultEntry, headEntry]) if (entry) branches.set(entry.branchName, entry); const rank = (entry) => entry.ref === defaultEntry?.ref ? 0 : entry.ref === headEntry?.ref ? 1 : 2; return { branches: [...branches.values()].toSorted((a, b) => rank(a) - rank(b) || a.branch.name.localeCompare(b.branch.name)).map((entry) => entry.branch), ...defaultEntry ? { defaultBranch: defaultEntry.branch.name } : {}, ...headEntry ? { headBranch: headEntry.branch.name } : {}, ...options.includeRepositoryStatus ? { repositoryStatus: "git" } : {}, ...branchesUnavailable ? { branchesUnavailable: true } : {} }; } //#endregion //#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; } } async function gitOutput(cwd, args) { try { const result = await runGit(cwd, args); return result.code === 0 ? result.stdout.trim() || null : null; } catch { return 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 runGit(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 { 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) { try { return (await runGit(root, [ "merge-base", "--is-ancestor", ancestor, descendant ])).code === 0; } catch { return false; } } /** 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) return null; if (unique.length === 1) return first; 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 /** File-backed Git metadata is checked in the worker, never by spawning Git. */ function readCheckoutGitRevision({ root, includeIndex }) { if (!canReadGitFilesystemRefs()) return null; try { const head = readGitHead(root, { maxDepth: 1 }); if (!head) return null; const gitDir = path.dirname(head.headPath); const common = resolveGitRefsBase(head.headPath); if (fs.existsSync(path.join(common, "reftable"))) return null; const paths = [ path.join(root, ".git"), head.headPath, path.join(gitDir, "commondir"), path.join(common, "config"), path.join(gitDir, "config.worktree"), path.join(common, "packed-refs") ]; if (includeIndex) paths.push(path.join(gitDir, "index")); const refs = path.join(common, "refs"); if (fs.existsSync(refs)) paths.push(...fs.readdirSync(refs, { recursive: true, encoding: "utf8" }).map((name) => path.join(refs, name))); return JSON.stringify(paths.toSorted().map((file) => { const stat = fs.lstatSync(file, { throwIfNoEntry: false }); if (stat?.isSymbolicLink()) throw new Error("Symbolic Git metadata requires Git discovery"); return [ 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) { 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 remote = remoteUrl ? parseGitHubRemoteUrl(remoteUrl) : 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, 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 paths = (await gitOutput(root, [ "ls-files", "--others", "--exclude-standard", "-z" ]) ?? "").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) { try { const result = await runGit(root, [ "-c", "diff.autoRefreshIndex=false", "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 { return null; } } /** * GitHub's pull/new page only has something to offer once the pushed branch * carries commits the default branch lacks. Rename-only commits still count: * this gate keys on commits, not line counts. */ async function branchHasCreatablePullRequest(root, defaultSha, pushedSha, defaultBranch) { if (!defaultBranch || !pushedSha) return false; if (!defaultSha) return true; const ahead = await gitOutput(root, [ "rev-list", "--count", `${defaultSha}..${pushedSha}` ]); return ahead === null || Number(ahead) > 0; } async function readPullRequestBranchFacts(input) { const landing = await resolveBranchLanding(input.root, input); const stats = landing.statsBase ? await diffStatsAgainst(input.root, landing.statsBase) : null; const noPushedChanges = stats !== null && landing.defaultSha !== null && landing.defaultSha === landing.pushedSha && landing.statsBase === landing.defaultSha; const creatable = (!landing.hasLandedPullRequest || landing.provenNewPushedWork) && !noPushedChanges && await branchHasCreatablePullRequest(input.root, landing.defaultSha, landing.pushedSha, input.defaultBranch); return !creatable && !(stats && stats.changedFiles > 0) ? void 0 : { creatable, stats }; } //#endregion //#region src/sessions/session-diff-parser.ts /** Parses `git diff --name-status -z -M` output; R/C entries consume two paths. */ function parseNameStatusZ(text) { return parseNameStatusTokens(text.split("\0")); } function parseNameStatusTokens(tokens) { const entries = []; for (let i = 0; i < tokens.length; i += 1) { const code = tokens[i]; if (!code) continue; const letter = code[0]; if (letter === "R" || letter === "C") { const oldPath = tokens[i + 1]; const path = tokens[i + 2]; i += 2; if (path) entries.push({ path, oldPath, status: letter === "R" ? "renamed" : "added" }); continue; } const path = tokens[i + 1]; i += 1; if (!path) continue; const status = letter === "A" ? "added" : letter === "D" ? "deleted" : "modified"; entries.push({ path, status }); } return entries; } /** Parses `git diff --numstat -z -M`; rename entries put paths in follow-up tokens. */ function parseNumstatZ(text) { return parseNumstatTokens(text.split("\0"), 0); } function parseNumstatTokens(tokens, start) { const byPath = /* @__PURE__ */ new Map(); for (let i = start; i < tokens.length; i += 1) { const token = tokens[i]; if (!token) continue; const [added, deleted, ...pathParts] = token.split(" "); const inlinePath = pathParts.join(" "); if (added === void 0 || deleted === void 0) continue; const binary = added === "-"; const entry = { additions: binary ? 0 : Number.parseInt(added, 10) || 0, deletions: binary ? 0 : Number.parseInt(deleted, 10) || 0, binary }; if (inlinePath) { byPath.set(inlinePath, entry); continue; } const path = tokens[i + 2]; i += 2; if (path) byPath.set(path, entry); } return byPath; } /** Git emits all raw records before the numstat records in `--raw --numstat -z`. */ function parseDiffInventoryZ(text) { const tokens = text.split("\0"); const nameStatus = []; let index = 0; while (index < tokens.length) { const header = tokens[index]; if (!header?.startsWith(":")) break; const code = header.slice(header.lastIndexOf(" ") + 1); const pathCount = code[0] === "R" || code[0] === "C" ? 2 : 1; nameStatus.push(code, ...tokens.slice(index + 1, index + pathCount + 1)); index += pathCount + 1; } return { entries: parseNameStatusTokens(nameStatus), numstat: parseNumstatTokens(tokens, index) }; } const GIT_PATH_ESCAPES = { a: "\x07", b: "\b", f: "\f", n: "\n", r: "\r", t: " ", v: "\v" }; function decodeGitPath(value) { if (!value.startsWith("\"")) return value; const decoded = Buffer.from(value.slice(1, -1)).toString("latin1").replace(/\\([0-3][0-7]{2}|[abfnrtv"\\])/g, (_, escape) => escape.length === 3 ? String.fromCharCode(Number.parseInt(escape, 8)) : GIT_PATH_ESCAPES[escape] ?? escape); return Buffer.from(decoded, "latin1").toString("utf8"); } function chunkPath(chunk) { const newFile = /(?:^|\n)\+\+\+ (b\/[^\t\n]+|"b\/[^\n]+")\t?(?:\n|$)/.exec(chunk); if (newFile) return decodeGitPath(expectDefined(newFile[1], "new file capture group 1")).slice(2); const oldFile = /(?:^|\n)--- (a\/[^\t\n]+|"a\/[^\n]+")\t?(?:\n|$)/.exec(chunk); if (oldFile) return decodeGitPath(expectDefined(oldFile[1], "old file capture group 1")).slice(2); const renameTo = /(?:^|\n)rename to ([^\n]+)(?:\n|$)/.exec(chunk); if (renameTo) return decodeGitPath(expectDefined(renameTo[1], "rename to capture group 1")); const header = /(?:^|\n)diff --git (?:"a\/((?:\\.|[^"\\\n])*)" "b\/\1"|a\/([^\n]+) b\/\2)(?:\n|$)/.exec(chunk); if (!header) return null; return header[1] === void 0 ? expectDefined(header[2], "header capture group 2") : decodeGitPath(`"${header[1]}"`); } /** Splits a multi-file `git diff --patch` into per-file chunks keyed by path. */ function splitPatchByFile(patch) { const byPath = /* @__PURE__ */ new Map(); if (!patch.trim()) return byPath; const parts = patch.split(/(?<=\n)(?=diff --git )/); for (const part of parts) { if (!part.startsWith("diff --git ")) continue; const path = chunkPath(part); if (path) byPath.set(path, part); } return byPath; } //#endregion //#region src/sessions/session-diff-revisions.ts /** Picks the merge base used for branch-relative session diffs. */ async function resolveSessionDiffBase(params) { const remoteDefault = (await params.gitOut(params.root, [ "symbolic-ref", "--short", "refs/remotes/origin/HEAD" ]))?.trim() || null; const defaultShort = remoteDefault?.replace(/^origin\//, ""); if (remoteDefault && defaultShort && params.branch && params.branch !== defaultShort) { const mergeBase = await params.gitOut(params.root, [ "merge-base", remoteDefault, params.head ]); if (mergeBase?.trim()) return { base: mergeBase.trim(), baseRef: defaultShort }; } if (params.branch && params.branch !== "main" && params.branch !== "master") for (const candidate of [ "main", "master", "origin/main", "origin/master" ]) { const verified = await params.gitOut(params.root, [ "rev-parse", "--verify", "--quiet", candidate ]); if (verified?.trim()) { const mergeBase = await params.gitOut(params.root, [ "merge-base", verified.trim(), params.head ]); if (mergeBase?.trim()) return { base: mergeBase.trim(), baseRef: candidate }; } } return { base: params.head, baseRef: "HEAD" }; } /** Resolves the repository-format-specific empty tree without writing it. */ async function resolveSessionDiffEmptyTree(root, objectFormat) { if (objectFormat === "sha1") return { base: "4b825dc642cb6eb9a060e54bf8d69288fbee4904" }; if (objectFormat === "sha256") return { base: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321" }; try { const result = await runGit(root, [ "hash-object", "-t", "tree", "--stdin" ], { input: "" }); const emptyTree = result.code === 0 ? result.stdout.trim() : ""; return emptyTree ? { base: emptyTree } : null; } catch { return null; } } function parseCommitRecord(line) { const separator = line.indexOf("\0"); if (separator <= 0) return; return { sha: line.slice(0, separator), subject: line.slice(separator + 1) }; } function parseCommitRecords(text) { return text.split("\n").map(parseCommitRecord).filter((record) => record !== void 0); } /** Loads the bounded branch history metadata shared by every diff scope. */ async function loadSessionDiffBranchMetadata(params) { if (params.base === "HEAD" || params.base === params.head) return {}; const range = `${params.base}..${params.head}`; const [aheadText, commitsText, mergeBaseText] = await Promise.all([ params.gitOut(params.root, [ "rev-list", "--count", range ]), params.gitOut(params.root, [ "log", "--max-count=50", "--format=%h%x00%s", range, "--" ]), params.gitOut(params.root, [ "show", "--no-patch", "--format=%h%x00%s", params.base, "--" ]) ]); const normalizedAhead = aheadText?.trim(); const aheadCount = normalizedAhead && /^\d+$/.test(normalizedAhead) ? Number.parseInt(normalizedAhead, 10) : void 0; const mergeBase = mergeBaseText ? parseCommitRecords(mergeBaseText)[0] : void 0; return { ...aheadCount !== void 0 ? { aheadCount } : {}, ...commitsText !== null ? { commits: parseCommitRecords(commitsText) } : {}, ...mergeBase ? { mergeBase } : {} }; } //#endregion //#region src/sessions/session-diff.runtime.ts const MAX_FILES = 500; const MAX_UNTRACKED_FILES = 100; const MAX_PATCH_BYTES_PER_FILE = 1e5; const MAX_TOTAL_PATCH_BYTES = 15e5; const MAX_BASELINE_GIT_OUTPUT_BYTES = 512e3; const MAX_BASELINE_FILE_BYTES = 4194304; const MAX_BASELINE_TOTAL_BYTES = 16777216; const MAX_TOTAL_CHANGED_LINES = 1e5; const PATCH_DIFF_ARGS = [ "--patch", "--no-color", "--no-ext-diff", "--no-textconv", "--src-prefix=a/", "--dst-prefix=b/" ]; async function gitOut(cwd, args, okCodes = [0]) { try { const result = await runGit(cwd, [ "-c", "core.quotePath=false", ...args ]); return okCodes.includes(result.code ?? -1) ? result.stdout : null; } catch { return null; } } async function loadCheckoutRevision(cwd) { try { const result = await runGit(cwd, [ "rev-parse", "--show-object-format", "--show-toplevel", "--verify", "--quiet", "HEAD" ]); if (result.termination !== "exit" || result.code !== 0 && result.code !== 1) return; const lines = result.stdout.replace(/\n$/, "").split("\n"); const objectFormat = lines.shift(); const head = result.code === 0 ? lines.pop() : void 0; const root = lines.join("\n"); if (!root) return; const branchOut = head ? (await gitOut(root, [ "rev-parse", "--abbrev-ref", "HEAD" ]))?.trim() : void 0; return { root, head, branch: branchOut && branchOut !== "HEAD" ? branchOut : void 0, objectFormat }; } catch { return; } } function readPatchHeader(chunk) { const header = /(?:^|\n)(@@ [^\n]*|Binary files [^\n]* differ|GIT binary patch)\n/.exec(chunk)?.[1]; const added = /^@@ -0,0 \+1(?:,(\d+))? @@(?: |$)/.exec(header ?? ""); return { ...added ? { additions: Number(added[1] ?? 1) } : {}, binary: header !== void 0 && !header.startsWith("@@ ") }; } /** * A patch-producing `git diff` reads working-tree file contents, so a * checkout-planted hardlink to an out-of-tree secret would otherwise leak * through this read-scoped RPC (same threat the fs-safe workspace readers * reject). Content is only emitted for a real, single-linked regular file * whose realpath stays inside the checkout. Deleted files are exempt: git * reads their content from the object DB, never the filesystem. */ async function isPatchableWorkingTreePath(realRoot, relPath) { const abs = path.resolve(realRoot, relPath); try { const info = await fs$1.lstat(abs); if (!info.isFile() || info.nlink !== 1) return false; const resolved = await fs$1.realpath(abs); return resolved === realRoot || resolved.startsWith(realRoot + path.sep); } catch { return false; } } function takePatch(chunk, budget) { if (!chunk) return { truncated: true }; const bytes = Buffer.byteLength(chunk, "utf8"); if (bytes > MAX_PATCH_BYTES_PER_FILE || bytes > budget.remaining) return { truncated: true }; budget.remaining -= bytes; return { patch: chunk }; } async function collectUntrackedFiles(root, realRoot, budget) { const paths = (await gitOut(root, [ "ls-files", "--others", "--exclude-standard", "-z" ]) ?? "").split("\0").filter(Boolean); const truncated = paths.length > MAX_UNTRACKED_FILES; const files = []; for (const filePath of paths.slice(0, MAX_UNTRACKED_FILES)) { const file = { path: filePath, status: "added", additions: 0, deletions: 0, untracked: true }; files.push(file); if (!await isPatchableWorkingTreePath(realRoot, filePath)) { file.truncated = true; continue; } const result = await runGitBuffered(root, [ "-c", "core.quotePath=false", "diff", ...PATCH_DIFF_ARGS, "--no-index", "--", "/dev/null", filePath ], { timeoutMs: GIT_TIMEOUT_MS, maxOutputBytes: MAX_PATCH_BYTES_PER_FILE, discardOutput: { stderr: true } }); const patchTruncated = result.termination === "output-limit" && result.outputLimitStream === "stdout"; if (!patchTruncated && (result.termination !== "exit" || result.code !== 0 && result.code !== 1)) { file.truncated = true; continue; } const patch = result.stdout.toString("utf8"); if (!patch.startsWith("diff --git ")) { file.truncated = true; continue; } const header = readPatchHeader(patch); file.additions = header.additions ?? 0; if (header.binary) { file.binary = true; continue; } Object.assign(file, takePatch(patchTruncated ? void 0 : patch, budget)); } return { files, truncated }; } async function collectTrackedFiles(root, realRoot, revisions, budget) { const diffArgs = (options) => [ "diff", "-M", ...options, ...revisions, "--" ]; const inventoryText = await gitOut(root, diffArgs([ "--raw", "--numstat", "--no-color", "-z" ])); let inventory; if (inventoryText !== null) inventory = parseDiffInventoryZ(inventoryText); else { const entries = parseNameStatusZ(await gitOut(root, diffArgs(["--name-status", "-z"])) ?? ""); if (entries.length === 0) return { files: [], truncated: false }; inventory = { entries, numstat: parseNumstatZ(await gitOut(root, diffArgs(["--numstat", "-z"])) ?? "") }; } const { entries, numstat } = inventory; if (entries.length === 0) return { files: [], truncated: false }; const patchText = [...numstat.values()].reduce((sum, entry) => sum + entry.additions + entry.deletions, 0) > MAX_TOTAL_CHANGED_LINES ? null : await gitOut(root, diffArgs(PATCH_DIFF_ARGS)); const chunks = patchText === null ? /* @__PURE__ */ new Map() : splitPatchByFile(patchText); const truncated = entries.length > MAX_FILES; const files = []; for (const entry of entries.slice(0, MAX_FILES)) { const stat = numstat.get(entry.path); const chunk = chunks.get(entry.path); const binary = stat?.binary === true || chunk !== void 0 && readPatchHeader(chunk).binary; const file = { path: entry.path, status: entry.status, additions: stat?.additions ?? 0, deletions: stat?.deletions ?? 0 }; if (entry.oldPath) file.oldPath = entry.oldPath; if (binary) { file.binary = true; files.push(file); continue; } if (!(revisions.length === 2 || entry.status === "deleted" || await isPatchableWorkingTreePath(realRoot, entry.path))) { file.truncated = true; files.push(file); continue; } const taken = takePatch(chunk, budget); if (taken.patch !== void 0) file.patch = taken.patch; if (taken.truncated) file.truncated = true; files.push(file); } return { files, truncated }; } async function collectCheckoutDiff(params) { const empty = (unavailableReason) => ({ files: [], additions: 0, deletions: 0, ...unavailableReason ? { unavailableReason } : {} }); const checkout = await loadCheckoutRevision(params.cwd); if (!checkout) return empty("not_git"); const { root, head, branch, objectFormat } = checkout; const realRoot = await fs$1.realpath(root).catch(() => root); const branchBase = params.baseCommit ? { base: params.baseCommit, baseRef: params.baseCommit } : head ? await resolveSessionDiffBase({ branch, gitOut, head, root }) : await resolveSessionDiffEmptyTree(root, objectFormat); const metadata = head && branchBase ? await loadSessionDiffBranchMetadata({ base: branchBase.base, gitOut, head, root }) : {}; const repositoryFields = { root, ...branch ? { branch } : {}, ...branchBase?.baseRef ? { baseRef: branchBase.baseRef } : {}, ...metadata }; const unknownCommit = () => ({ ...repositoryFields, files: [], additions: 0, deletions: 0, unavailableReason: "unknown_commit" }); const scope = params.scope ?? "all"; let revisions; if (scope === "commit") { if (!head || !branchBase || branchBase.base === "HEAD" || branchBase.base === head) return unknownCommit(); const commit = (await gitOut(root, [ "rev-parse", "--verify", "--quiet", "--end-of-options", `${params.commit}^{commit}` ]))?.trim(); if (!commit) return unknownCommit(); const isCommitInHeadHistory = await gitOut(root, [ "merge-base", "--is-ancestor", commit, head ], [0]) !== null; const isCommitInBaseHistory = await gitOut(root, [ "merge-base", "--is-ancestor", commit, branchBase.base ], [0]) !== null; if (!isCommitInHeadHistory || isCommitInBaseHistory) return unknownCommit(); const parent = (await gitOut(root, [ "rev-parse", "--verify", "--quiet", `${commit}^` ]))?.trim(); const commitBase = parent ? { base: parent } : await resolveSessionDiffEmptyTree(root, objectFormat); revisions = commitBase ? [commitBase.base, commit] : void 0; } else if (scope === "uncommitted") revisions = head ? [head] : branchBase ? [branchBase.base] : void 0; else revisions = branchBase ? [branchBase.base] : void 0; const budget = { remaining: MAX_TOTAL_PATCH_BYTES }; const tracked = revisions ? await collectTrackedFiles(root, realRoot, revisions, budget) : { files: [], truncated: false }; const untracked = scope === "commit" ? { files: [], truncated: false } : await collectUntrackedFiles(root, realRoot, budget); const files = [...tracked.files, ...untracked.files].toSorted((a, b) => a.path.localeCompare(b.path)); const additions = files.reduce((sum, file) => sum + file.additions, 0); const deletions = files.reduce((sum, file) => sum + file.deletions, 0); const truncated = tracked.truncated || untracked.truncated || files.some((file) => file.truncated === true); const diff = { ...repositoryFields, files, additions, deletions, ...truncated ? { truncated: true } : {} }; return params.sessionId ? await applySessionDiffBaseline({ baseline: params.baseline, diff, sessionId: params.sessionId }) : diff; } function hashBaselineDescriptor(candidate, content, bytes) { return crypto.createHash("sha256").update([ candidate.path, candidate.oldPath ?? "", candidate.status, candidate.untracked === true ? "untracked" : "tracked", content ].join("\0")).update(bytes ?? "").digest("hex"); } async function fingerprintBaselineCandidate(params) { try { var _usingCtx$1 = _usingCtx(); const { candidate } = params; if (candidate.status === "deleted") return hashBaselineDescriptor(candidate, "deleted"); const absolutePath = path.resolve(params.root, candidate.path); const relativePath = path.relative(params.root, absolutePath); if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return; const initial = await fs$1.lstat(absolutePath).catch(() => void 0); if (!initial) return; if (initial.isSymbolicLink()) { const target = await fs$1.readlink(absolutePath).catch(() => void 0); return target === void 0 ? void 0 : hashBaselineDescriptor(candidate, `symlink:${target}`); } const opened = await params.directory?.open(`.${path.sep}${relativePath}`).catch(() => void 0); if (!opened) return; const file = _usingCtx$1.a(opened); const { mode, size } = file.stat; if (size > MAX_BASELINE_FILE_BYTES || size > params.budget.remaining) return; params.budget.remaining -= size; const buffer = Buffer.allocUnsafe(size); if (await readFileWindowFully(file.handle, buffer, 0) !== size) return; return hashBaselineDescriptor(candidate, `${mode}\0${size}`, buffer); } catch (_) { _usingCtx$1.e = _; } finally { await _usingCtx$1.d(); } } async function gitOutForBaseline(cwd, args) { const result = await runGitBuffered(cwd, [ "-c", "core.quotePath=false", ...args ], { timeoutMs: 3e4, maxOutputBytes: { stdout: MAX_BASELINE_GIT_OUTPUT_BYTES, stderr: 32768 } }); if (result.termination !== "exit" || result.code !== 0) return null; return result.stdout.toString("utf8"); } async function collectBaselineCandidates(params) { const checkout = await loadCheckoutRevision(params.cwd); if (!checkout) return; const { root, head, branch, objectFormat } = checkout; const baseInfo = head ? await resolveSessionDiffBase({ branch, gitOut, head, root }) : await resolveSessionDiffEmptyTree(root, objectFormat); const [trackedResult, untrackedResult] = await Promise.allSettled([baseInfo ? gitOutForBaseline(root, [ "diff", "-M", baseInfo.base, "--name-status", "-z" ]) : Promise.resolve(""), gitOutForBaseline(root, [ "ls-files", "--others", "--exclude-standard", "-z" ])]); if (trackedResult.status === "rejected") throw trackedResult.reason; if (untrackedResult.status === "rejected") throw untrackedResult.reason; const trackedText = trackedResult.value; const untrackedText = untrackedResult.value; if (trackedText === null || untrackedText === null) return { root, candidates: [], truncated: true }; const tracked = parseNameStatusZ(trackedText); const untrackedPaths = untrackedText.split("\0").filter(Boolean); return { root, candidates: [...tracked.slice(0, MAX_FILES), ...untrackedPaths.slice(0, MAX_UNTRACKED_FILES).map((path) => ({ path, status: "added", untracked: true }))].toSorted((left, right) => left.path.localeCompare(right.path)), truncated: tracked.length > MAX_FILES || untrackedPaths.length > MAX_UNTRACKED_FILES }; } async function fingerprintBaselineCandidates(params) { const directory = await root(params.root, { hardlinks: "reject", symlinks: "follow-parents-within-root" }).catch(() => void 0); const budget = { remaining: MAX_BASELINE_TOTAL_BYTES }; const files = []; for (const candidate of params.candidates) { const fingerprint = await fingerprintBaselineCandidate({ budget, candidate, directory, root: params.root }); if (fingerprint) files.push({ path: candidate.path, fingerprint }); } return { files, truncated: files.length !== params.candidates.length }; } async function collectCheckoutDiffBaseline(params) { const collected = await collectBaselineCandidates({ cwd: params.cwd }); if (!collected) return; const fingerprinted = await fingerprintBaselineCandidates({ candidates: collected.candidates, root: collected.root }); return { version: 1, root: collected.root, files: fingerprinted.files, ...collected.truncated || fingerprinted.truncated ? { truncated: true } : {} }; } async function applySessionDiffBaseline(params) { const { baseline, diff } = params; if (baseline?.version !== 1 || baseline.sessionId !== params.sessionId || !diff.root || baseline.root !== diff.root) return diff; const fingerprints = new Map(baseline.files.map((file) => [file.path, file.fingerprint])); const current = await fingerprintBaselineCandidates({ candidates: diff.files.filter((file) => fingerprints.has(file.path)), root: diff.root }); const currentFingerprints = new Map(current.files.map((file) => [file.path, file.fingerprint])); const files = diff.files.filter((file) => { const baselineFingerprint = fingerprints.get(file.path); return !baselineFingerprint || currentFingerprints.get(file.path) !== baselineFingerprint; }); if (files.length === diff.files.length) return diff; return { ...diff, files, additions: files.reduce((sum, file) => sum + file.additions, 0), deletions: files.reduce((sum, file) => sum + file.deletions, 0) }; } //#endregion //#region src/infra/git-read-operations.runtime.ts async function executeGitReadOperation(operation) { switch (operation.type) { case "checkout.revision": return readCheckoutGitRevision(operation.input); case "checkout.context": return await readCheckoutGitContext(operation.input.root); case "checkout.diff": return await collectCheckoutDiff(operation.input); case "checkout.baseline": return await collectCheckoutDiffBaseline(operation.input); case "repository.branches": return await readRepositoryBranches(operation.input.repoRoot, operation.input); case "pull-request.branch-facts": return await readPullRequestBranchFacts(operation.input); } throw new Error("Unsupported Git read operation"); } //#endregion export { executeGitReadOperation };