From 7f6408c397e21c8e9dd6c461a19a71e09e8bdf96 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 1 Nov 2026 08:39:23 -0800 Subject: [PATCH] fix(land): keep git maintenance from racing the fixer import A fixer job crashed with ENOENT on a vanished objects/maintenance.lock (moq-dev/moq#6987): background maintenance removed the lock between importFixer's readdir and lstat. Fixer checkouts now turn off gc.auto and maintenance.auto, and the import skips git lock files, which it never imports. Co-Authored-By: Claude Opus 5.5 --- src/cli/land-fixer-repository.ts | 3 +++ src/cli/land-fixer.ts | 3 +++ test/land.test.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/cli/land-fixer-repository.ts b/src/cli/land-fixer-repository.ts index 238b2f83..8113517b 100644 --- a/src/cli/land-fixer-repository.ts +++ b/src/cli/land-fixer-repository.ts @@ -41,6 +41,9 @@ export function importFixer( const objects: string[] = []; const walk = (path: string, relative = "") => { for (const name of readdirSync(path)) { + // Git lock files are transient: a background `git maintenance` or auto-gc can create and remove one + // while this walks (moq-dev/moq#6987 crashed on a vanished `objects/maintenance.lock`). None is imported. + if (name.endsWith(".lock")) continue; if (++files > 100_000) throw new Error("fixer repository has too many files"); const child = join(path, name); const rel = relative ? `${relative}/${name}` : name; diff --git a/src/cli/land-fixer.ts b/src/cli/land-fixer.ts index 0ce9e9e0..9597f4db 100644 --- a/src/cli/land-fixer.ts +++ b/src/cli/land-fixer.ts @@ -180,6 +180,9 @@ export async function startFixer( return; } git(checkout, ["init", "--quiet"]); + // No background gc or maintenance may rewrite objects while the daemon imports the fixer's result. + git(checkout, ["config", "gc.auto", "0"]); + git(checkout, ["config", "maintenance.auto", "false"]); git(checkout, [ "fetch", "--quiet", diff --git a/test/land.test.ts b/test/land.test.ts index f2199028..3fe82312 100644 --- a/test/land.test.ts +++ b/test/land.test.ts @@ -2074,6 +2074,8 @@ describe.skipIf(process.platform === "win32")("asynchronous land fixer", () => { mkdirSync(submodule, { recursive: true }); writeFileSync(join(submodule, `${large}.pack`), ""); truncateSync(join(submodule, `${large}.pack`), 101 * 1024 * 1024); + // A transient lock is skipped, never read: git maintenance may remove it mid-walk (moq-dev/moq#6987). + symlinkSync(join(checkout, "missing"), join(checkout, ".git", "objects", "maintenance.lock")); importFixer(root, tempDir(), checkout, 1, fixed, [...packs, `${large}.pack`]); expect(git(root, "show", `${fixed}:kept.txt`)).toBe("unchanged"); expect(git(root, "rev-parse", `${fixed}^`)).toBe(pinned);