mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Infra: unify git root discovery
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { findGitRoot } from "../infra/git-root.js";
|
||||
import {
|
||||
formatUserTime,
|
||||
resolveUserTimeFormat,
|
||||
@@ -92,24 +93,3 @@ function resolveRepoRoot(params: {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findGitRoot(startDir: string): string | null {
|
||||
let current = path.resolve(startDir);
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
const gitPath = path.join(current, ".git");
|
||||
try {
|
||||
const stat = fs.statSync(gitPath);
|
||||
if (stat.isDirectory() || stat.isFile()) {
|
||||
return current;
|
||||
}
|
||||
} catch {
|
||||
// ignore missing .git at this level
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { resolveGitHeadPath } from "./git-root.js";
|
||||
|
||||
const formatCommit = (value?: string | null) => {
|
||||
if (!value) {
|
||||
@@ -13,35 +14,6 @@ const formatCommit = (value?: string | null) => {
|
||||
return trimmed.length > 7 ? trimmed.slice(0, 7) : trimmed;
|
||||
};
|
||||
|
||||
const resolveGitHead = (startDir: string) => {
|
||||
let current = startDir;
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
const gitPath = path.join(current, ".git");
|
||||
try {
|
||||
const stat = fs.statSync(gitPath);
|
||||
if (stat.isDirectory()) {
|
||||
return path.join(gitPath, "HEAD");
|
||||
}
|
||||
if (stat.isFile()) {
|
||||
const raw = fs.readFileSync(gitPath, "utf-8");
|
||||
const match = raw.match(/gitdir:\s*(.+)/i);
|
||||
if (match?.[1]) {
|
||||
const resolved = path.resolve(current, match[1].trim());
|
||||
return path.join(resolved, "HEAD");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore missing .git at this level
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let cachedCommit: string | null | undefined;
|
||||
|
||||
const readCommitFromPackageJson = () => {
|
||||
@@ -102,7 +74,7 @@ export const resolveCommitHash = (options: { cwd?: string; env?: NodeJS.ProcessE
|
||||
return cachedCommit;
|
||||
}
|
||||
try {
|
||||
const headPath = resolveGitHead(options.cwd ?? process.cwd());
|
||||
const headPath = resolveGitHeadPath(options.cwd ?? process.cwd());
|
||||
if (!headPath) {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
|
||||
59
src/infra/git-root.test.ts
Normal file
59
src/infra/git-root.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findGitRoot, resolveGitHeadPath } from "./git-root.js";
|
||||
|
||||
async function makeTempDir(label: string): Promise<string> {
|
||||
return fs.mkdtemp(path.join(os.tmpdir(), `openclaw-${label}-`));
|
||||
}
|
||||
|
||||
describe("git-root", () => {
|
||||
it("finds git root and HEAD path when .git is a directory", async () => {
|
||||
const temp = await makeTempDir("git-root-dir");
|
||||
const repoRoot = path.join(temp, "repo");
|
||||
const workspace = path.join(repoRoot, "nested", "workspace");
|
||||
await fs.mkdir(path.join(repoRoot, ".git"), { recursive: true });
|
||||
await fs.mkdir(workspace, { recursive: true });
|
||||
|
||||
expect(findGitRoot(workspace)).toBe(repoRoot);
|
||||
expect(resolveGitHeadPath(workspace)).toBe(path.join(repoRoot, ".git", "HEAD"));
|
||||
});
|
||||
|
||||
it("resolves HEAD path when .git is a gitdir pointer file", async () => {
|
||||
const temp = await makeTempDir("git-root-file");
|
||||
const repoRoot = path.join(temp, "repo");
|
||||
const workspace = path.join(repoRoot, "nested", "workspace");
|
||||
const gitDir = path.join(repoRoot, ".actual-git");
|
||||
await fs.mkdir(workspace, { recursive: true });
|
||||
await fs.mkdir(gitDir, { recursive: true });
|
||||
await fs.writeFile(path.join(repoRoot, ".git"), "gitdir: .actual-git\n", "utf-8");
|
||||
|
||||
expect(findGitRoot(workspace)).toBe(repoRoot);
|
||||
expect(resolveGitHeadPath(workspace)).toBe(path.join(gitDir, "HEAD"));
|
||||
});
|
||||
|
||||
it("keeps root detection for .git file and skips invalid gitdir content for HEAD lookup", async () => {
|
||||
const temp = await makeTempDir("git-root-invalid-file");
|
||||
const parentRoot = path.join(temp, "repo");
|
||||
const childRoot = path.join(parentRoot, "child");
|
||||
const nested = path.join(childRoot, "nested");
|
||||
await fs.mkdir(path.join(parentRoot, ".git"), { recursive: true });
|
||||
await fs.mkdir(nested, { recursive: true });
|
||||
await fs.writeFile(path.join(childRoot, ".git"), "not-a-gitdir-pointer\n", "utf-8");
|
||||
|
||||
expect(findGitRoot(nested)).toBe(childRoot);
|
||||
expect(resolveGitHeadPath(nested)).toBe(path.join(parentRoot, ".git", "HEAD"));
|
||||
});
|
||||
|
||||
it("respects maxDepth traversal limit", async () => {
|
||||
const temp = await makeTempDir("git-root-depth");
|
||||
const repoRoot = path.join(temp, "repo");
|
||||
const nested = path.join(repoRoot, "a", "b", "c");
|
||||
await fs.mkdir(path.join(repoRoot, ".git"), { recursive: true });
|
||||
await fs.mkdir(nested, { recursive: true });
|
||||
|
||||
expect(findGitRoot(nested, { maxDepth: 2 })).toBeNull();
|
||||
expect(resolveGitHeadPath(nested, { maxDepth: 2 })).toBeNull();
|
||||
});
|
||||
});
|
||||
72
src/infra/git-root.ts
Normal file
72
src/infra/git-root.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user