Files
openclaw/src/infra/git-root.ts
2026-02-18 00:45:43 -05:00

73 lines
2.0 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
export const DEFAULT_GIT_DISCOVERY_MAX_DEPTH = 12;
function walkUpFrom<T>(
startDir: string,
opts: { maxDepth?: number },
resolveAtDir: (dir: string) => T | null | undefined,
): T | null {
let current = path.resolve(startDir);
const maxDepth = opts.maxDepth ?? DEFAULT_GIT_DISCOVERY_MAX_DEPTH;
for (let i = 0; i < maxDepth; i += 1) {
const resolved = resolveAtDir(current);
if (resolved !== null && resolved !== undefined) {
return resolved;
}
const parent = path.dirname(current);
if (parent === current) {
break;
}
current = parent;
}
return null;
}
function hasGitMarker(repoRoot: string): boolean {
const gitPath = path.join(repoRoot, ".git");
try {
const stat = fs.statSync(gitPath);
return stat.isDirectory() || stat.isFile();
} catch {
return false;
}
}
export function findGitRoot(startDir: string, opts: { maxDepth?: number } = {}): string | null {
// A `.git` file counts as a repo marker even if it is not a valid gitdir pointer.
return walkUpFrom(startDir, opts, (repoRoot) => (hasGitMarker(repoRoot) ? repoRoot : null));
}
function resolveGitDirFromMarker(repoRoot: string): string | null {
const gitPath = path.join(repoRoot, ".git");
try {
const stat = fs.statSync(gitPath);
if (stat.isDirectory()) {
return gitPath;
}
if (!stat.isFile()) {
return null;
}
const raw = fs.readFileSync(gitPath, "utf-8");
const match = raw.match(/gitdir:\s*(.+)/i);
if (!match?.[1]) {
return null;
}
return path.resolve(repoRoot, match[1].trim());
} catch {
return null;
}
}
export function resolveGitHeadPath(
startDir: string,
opts: { maxDepth?: number } = {},
): string | null {
// Stricter than findGitRoot: keep walking until a resolvable git dir is found.
return walkUpFrom(startDir, opts, (repoRoot) => {
const gitDir = resolveGitDirFromMarker(repoRoot);
return gitDir ? path.join(gitDir, "HEAD") : null;
});
}