mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 13:41:34 +00:00
133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { runCommandWithTimeout } from "../../process/exec.js";
|
|
|
|
const GIT_TIMEOUT_MS = 120_000;
|
|
|
|
export type GitResult = {
|
|
stdout: string;
|
|
stderr: string;
|
|
code: number | null;
|
|
};
|
|
|
|
type WorktreeListEntry = {
|
|
path: string;
|
|
lockedReason?: string;
|
|
};
|
|
|
|
export async function runGit(
|
|
cwd: string,
|
|
args: string[],
|
|
options: { env?: NodeJS.ProcessEnv; input?: string } = {},
|
|
): Promise<GitResult> {
|
|
return await runCommandWithTimeout(["git", "-C", cwd, ...args], {
|
|
timeoutMs: GIT_TIMEOUT_MS,
|
|
env: options.env,
|
|
input: options.input,
|
|
});
|
|
}
|
|
|
|
export function commandError(command: string, result: GitResult): Error {
|
|
const detail = (result.stderr || result.stdout).trim().split("\n").slice(-12).join("\n");
|
|
return new Error(`${command} failed${detail ? `:\n${detail}` : ""}`);
|
|
}
|
|
|
|
export async function requireGit(
|
|
cwd: string,
|
|
args: string[],
|
|
options: { env?: NodeJS.ProcessEnv; input?: string } = {},
|
|
): Promise<string> {
|
|
const result = await runGit(cwd, args, options);
|
|
if (result.code !== 0) {
|
|
throw commandError(`git ${args.join(" ")}`, result);
|
|
}
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
export async function requireGitRaw(cwd: string, args: string[]): Promise<string> {
|
|
const result = await runGit(cwd, args);
|
|
if (result.code !== 0) {
|
|
throw commandError(`git ${args.join(" ")}`, result);
|
|
}
|
|
return result.stdout;
|
|
}
|
|
|
|
function parseWorktreeList(output: string): WorktreeListEntry[] {
|
|
const entries: WorktreeListEntry[] = [];
|
|
let current: WorktreeListEntry | undefined;
|
|
for (const field of output.split("\0")) {
|
|
if (!field) {
|
|
if (current) {
|
|
entries.push(current);
|
|
current = undefined;
|
|
}
|
|
continue;
|
|
}
|
|
if (field.startsWith("worktree ")) {
|
|
if (current) {
|
|
entries.push(current);
|
|
}
|
|
current = { path: field.slice("worktree ".length) };
|
|
} else if (current && field === "locked") {
|
|
current.lockedReason = "";
|
|
} else if (current && field.startsWith("locked ")) {
|
|
current.lockedReason = field.slice("locked ".length);
|
|
}
|
|
}
|
|
if (current) {
|
|
entries.push(current);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
export async function listGitWorktrees(repoRoot: string): Promise<WorktreeListEntry[]> {
|
|
return parseWorktreeList(
|
|
await requireGitRaw(repoRoot, ["worktree", "list", "--porcelain", "-z"]),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* True when dir sits inside a git checkout: a .git entry on itself or any ancestor.
|
|
* Existence, not directory-ness, is the signal — linked worktrees keep a .git file.
|
|
* Mirrors `git rev-parse --show-toplevel` discovery without spawning git, so UI
|
|
* capability checks and create-preflights cannot diverge from the worktree service.
|
|
*/
|
|
export function insideGitCheckout(start: string): boolean {
|
|
let current = path.resolve(start);
|
|
for (;;) {
|
|
if (existsSync(path.join(current, ".git"))) {
|
|
return true;
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) {
|
|
return false;
|
|
}
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
export async function pathExists(target: string): Promise<boolean> {
|
|
try {
|
|
await fs.lstat(target);
|
|
return true;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
return false;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function removeEmptyParents(start: string, stop: string): Promise<void> {
|
|
let current = start;
|
|
while (current.startsWith(`${stop}${path.sep}`)) {
|
|
try {
|
|
await fs.rmdir(current);
|
|
} catch {
|
|
return;
|
|
}
|
|
current = path.dirname(current);
|
|
}
|
|
}
|