From 2287d65cda280cfa6238c330c9bddecb12213e13 Mon Sep 17 00:00:00 2001 From: wahaha1223 <0668001153@xydigit.com> Date: Thu, 23 Jul 2026 05:49:53 +0800 Subject: [PATCH] fix(infra): preserve git metadata across short reads (#109419) Co-authored-by: Peter Steinberger Co-authored-by: ZengWen-DT --- src/infra/git-commit.test.ts | 72 +++++++++++++++++++++++++++++++++++- src/infra/git-commit.ts | 3 +- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/infra/git-commit.test.ts b/src/infra/git-commit.test.ts index 1223497b1954..280614a12cee 100644 --- a/src/infra/git-commit.test.ts +++ b/src/infra/git-commit.test.ts @@ -1,4 +1,5 @@ // Covers git commit helper behavior in fake repositories. +import fsSync from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import process from "node:process"; @@ -56,6 +57,23 @@ async function makeFakeOpenClawPackage(root: string) { await fs.writeFile(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" })); } +function limitPositionalReads(maxBytes: number) { + const realReadSync = fsSync.readSync.bind(fsSync); + let totalBytesRead = 0; + vi.spyOn(fsSync, "readSync").mockImplementation((( + fd: number, + buffer: NodeJS.ArrayBufferView, + offset: number, + length: number, + position: number | null, + ) => { + const bytesRead = realReadSync(fd, buffer, offset, Math.min(length, maxBytes), position); + totalBytesRead += bytesRead; + return bytesRead; + }) as typeof fsSync.readSync); + return () => totalBytesRead; +} + describe("git commit resolution", () => { let resolveCommitHash: (typeof import("./git-commit.js"))["resolveCommitHash"]; @@ -210,7 +228,7 @@ describe("git commit resolution", () => { expect(resolveCommitHash({ cwd: repoA, env: {} })).toBe("0123456"); }); - it("reads packed refs from the common git dir for worktree-style checkouts", async () => { + it("reads packed refs after short commondir reads in worktree-style checkouts", async () => { const temp = await makeTempDir("git-commit-packed-refs"); const checkoutRoot = path.join(temp, "checkout"); const commonGitDir = path.join(temp, "git-common"); @@ -224,6 +242,7 @@ describe("git commit resolution", () => { "refs/heads/main": "0123456789abcdef0123456789abcdef01234567", }, }); + limitPositionalReads(4); expect(resolveCommitHash({ cwd: checkoutRoot, env: {} })).toBe("0123456"); }); @@ -381,6 +400,57 @@ describe("git commit resolution", () => { expect(resolveCommitHash({ cwd: repoRootValue, env: {} })).toBe("bbbbbbb"); }); + it("fills short positional reads for loose refs", async () => { + const temp = await makeTempDir("git-commit-short-ref"); + const repoRoot = path.join(temp, "repo"); + await makeFakeGitRepo(repoRoot, { + head: "ref: refs/heads/main\n", + refs: { + "refs/heads/main": "abcdef0123456789abcdef0123456789abcdef01", + }, + }); + limitPositionalReads(4); + + expect(resolveCommitHash({ cwd: repoRoot, env: {} })).toBe("abcdef0"); + }); + + it("keeps short-read retries within the bounded metadata window", async () => { + const temp = await makeTempDir("git-commit-bounded-ref"); + const repoRoot = path.join(temp, "repo"); + await makeFakeGitRepo(repoRoot, { + head: "ref: refs/heads/main\n", + refs: { + "refs/heads/main": `${"x".repeat(256)}abcdef0123456789`, + }, + }); + const totalBytesRead = limitPositionalReads(4); + + expect(resolveCommitHash({ cwd: repoRoot, env: {} })).toBeNull(); + expect(totalBytesRead()).toBe(256); + }); + + it("falls back to baked metadata when a bounded Git metadata read errors", async () => { + const temp = await makeTempDir("git-commit-read-error"); + const repoRoot = path.join(temp, "repo"); + await makeFakeGitRepo(repoRoot, { + head: "ref: refs/heads/main\n", + refs: { + "refs/heads/main": "abcdef0123456789abcdef0123456789abcdef01", + }, + }); + vi.spyOn(fsSync, "readSync").mockImplementationOnce((() => { + throw Object.assign(new Error("EIO: forced read failure"), { code: "EIO" }); + }) as typeof fsSync.readSync); + + expect( + resolveCommitHash({ + cwd: repoRoot, + env: {}, + readers: { readBuildInfoCommit: () => "deadbee" }, + }), + ).toBe("deadbee"); + }); + it("reads full HEAD refs before parsing long branch names", async () => { const temp = await makeTempDir("git-commit-long-head"); const repoRootLocal = path.join(temp, "repo"); diff --git a/src/infra/git-commit.ts b/src/infra/git-commit.ts index 66b0cab23d5e..d7799f123fb4 100644 --- a/src/infra/git-commit.ts +++ b/src/infra/git-commit.ts @@ -4,6 +4,7 @@ import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { readFileWindowFullySync } from "./file-read.js"; import { resolveGitHeadPath } from "./git-root.js"; import { pruneMapToMaxSize } from "./map-size.js"; import { resolveOpenClawPackageRootSync } from "./openclaw-root.js"; @@ -59,7 +60,7 @@ const safeReadFilePrefix = (filePath: string, limit = 256) => { const fd = fs.openSync(filePath, "r"); try { const buf = Buffer.alloc(limit); - const bytesRead = fs.readSync(fd, buf, 0, limit, 0); + const bytesRead = readFileWindowFullySync(fd, buf, 0); return buf.subarray(0, bytesRead).toString("utf-8"); } finally { fs.closeSync(fd);