fix(git-commit): cap commit resolution cache with FIFO eviction (#108856)

* fix(git-commit): cap commit resolution cache with FIFO eviction

The module-level cachedGitCommitBySearchDir Map grows without bound.
A 256-entry FIFO cap prevents unbounded memory growth from repeated
lookups across distinct search directories.

* fix(git-commit): use shared LRU cache pruning

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
wings1029
2026-07-17 01:54:35 +08:00
committed by GitHub
parent bf92404b50
commit d1153e4ad8
2 changed files with 48 additions and 1 deletions
+39
View File
@@ -288,6 +288,45 @@ describe("git commit resolution", () => {
expect(readGitCommit.mock.calls.length).toBe(firstCallReads);
});
it.each([
{ name: "successful", result: "abcdef0" },
{ name: "failed", result: null },
])("bounds $name git probe entries with LRU eviction", async ({ result }) => {
const temp = await makeTempDir(`git-commit-${result ? "success" : "failure"}-lru`);
const coldDir = path.join(temp, "cold");
const hotDir = path.join(temp, "hot");
const newestDir = path.join(temp, "newest");
const readGitCommit = vi.fn((_searchDir: string, _packageRoot: string | null) => result);
const resolve = (cwd: string) =>
resolveCommitHash({
cwd,
env: {},
readers: {
readGitCommit,
readBuildInfoCommit: () => null,
readPackageJsonCommit: () => null,
},
});
const callsFor = (cwd: string) =>
readGitCommit.mock.calls.filter(([searchDir]) => searchDir === cwd).length;
expect(resolve(coldDir)).toBe(result);
expect(resolve(hotDir)).toBe(result);
for (let index = 0; index < 254; index += 1) {
expect(resolve(path.join(temp, `filler-${index}`))).toBe(result);
}
expect(resolve(hotDir)).toBe(result);
expect(resolve(newestDir)).toBe(result);
expect(resolve(newestDir)).toBe(result);
expect(resolve(hotDir)).toBe(result);
expect(resolve(coldDir)).toBe(result);
expect(callsFor(newestDir)).toBe(1);
expect(callsFor(hotDir)).toBe(1);
expect(callsFor(coldDir)).toBe(2);
});
it("formats env-provided commit strings consistently", async () => {
const temp = await makeTempDir("git-commit-env");
expect(resolveCommitHash({ cwd: temp, env: { GIT_COMMIT: "ABCDEF0123456789" } })).toBe(
+9 -1
View File
@@ -5,6 +5,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { resolveGitHeadPath } from "./git-root.js";
import { pruneMapToMaxSize } from "./map-size.js";
import { resolveOpenClawPackageRootSync } from "./openclaw-root.js";
const formatCommit = (value?: string | null) => {
@@ -23,6 +24,7 @@ const formatCommit = (value?: string | null) => {
};
const cachedGitCommitBySearchDir = new Map<string, string | null>();
const GIT_COMMIT_CACHE_LIMIT = 256;
type CommitMetadataReaders = {
readGitCommit?: (searchDir: string, packageRoot: string | null) => string | null | undefined;
@@ -66,6 +68,7 @@ const safeReadFilePrefix = (filePath: string, limit = 256) => {
const cacheGitCommit = (searchDir: string, commit: string | null) => {
cachedGitCommitBySearchDir.set(searchDir, commit);
pruneMapToMaxSize(cachedGitCommitBySearchDir, GIT_COMMIT_CACHE_LIMIT);
return commit;
};
const resolveGitLookupDepth = (searchDir: string, packageRoot: string | null) => {
@@ -224,7 +227,12 @@ export const resolveCommitHash = (
}
const searchDir = resolveCommitSearchDir(options);
if (cachedGitCommitBySearchDir.has(searchDir)) {
return cachedGitCommitBySearchDir.get(searchDir) ?? null;
const cached = cachedGitCommitBySearchDir.get(searchDir) ?? null;
// Git discovery reads multiple files; keep active directories ahead of cold entries when
// the shared insertion-order pruning helper enforces the bound.
cachedGitCommitBySearchDir.delete(searchDir);
cachedGitCommitBySearchDir.set(searchDir, cached);
return cached;
}
const packageRoot = resolveOpenClawPackageRootSync({
cwd: options.cwd,