fix(reference): keep unresolved root and Git directory reads as failures

This commit is contained in:
morluto
2026-10-24 02:39:16 +08:00
parent bbc75545ec
commit 1ddb32956a
4 changed files with 58 additions and 7 deletions
@@ -41,20 +41,20 @@ export const locateReferenceSourceGitDirectory = async (
// Derived joins and the Git library normalize paths lexically. Admit the
// physical directory first, while the source pointer stays in its file.
const gitDirectory = await realpath(selectedGitDirectory);
let commonDirectory = gitDirectory;
let common: string | undefined;
try {
const common = (await readText(join(gitDirectory, "commondir"))).replace(
common = (await readText(join(gitDirectory, "commondir"))).replace(
/\r?\n$/u,
"",
);
if (common.length === 0)
throw new Error("Invalid Git common-directory pointer");
commonDirectory = await realpath(
absoluteCallerPath(common, { cwd: gitDirectory }),
);
} catch (cause: unknown) {
if (!hasCode(cause, "ENOENT")) throw cause;
}
if (common === "") throw new Error("Invalid Git common-directory pointer");
const commonDirectory =
common === undefined
? gitDirectory
: await realpath(absoluteCallerPath(common, { cwd: gitDirectory }));
return { repository, gitDirectory, commonDirectory };
}
};
+7
View File
@@ -91,6 +91,13 @@ const readSource = async (
bytesRead: state.bytesRead,
limitations: [PATH_RACE_LIMITATION],
};
if (traversal.ok && state.rootFailure?.code === "io")
return err({
tag: "reference-source-reader",
code: "io",
message: state.rootFailure.message,
partial: read,
});
return traversal.ok ? ok(read) : err({ ...traversal.error, partial: read });
};
@@ -155,3 +155,20 @@ it.skipIf(process.platform === "win32")(
expect(graph.vcs).toMatchObject({ kind: "git", head: intendedHead });
},
);
it("keeps a present but unresolved Git common-directory pointer from selecting private decoy configuration", async () => {
const root = await createTestTempDirectory("rea-reference-common-missing-");
await mkdir(join(root, ".git"));
await writeFile(join(root, ".git", "HEAD"), `${"1".repeat(40)}\n`);
await writeFile(join(root, ".git", "config"), "[core]\nignorecase = true\n");
await writeFile(join(root, ".git", "commondir"), "../missing-common\n");
const result = await importReferenceSource({
root,
caller: "ignore-policy-test",
policy: { secretPatterns: [] },
});
expect(result).toMatchObject({
ok: false,
error: { code: "io", message: expect.stringContaining("missing-common") },
});
});
@@ -20,6 +20,33 @@ const permissionChecksUnavailable =
process.platform === "win32" || process.getuid?.() === 0;
describe("reference source native unreadable directories", () => {
it.skipIf(permissionChecksUnavailable)(
"retains a root enumeration IO failure as an error and recovers after permission repair",
async () => {
const root = await createTestTempDirectory("rea-source-unreadable-root-");
await writeFile(join(root, "main.txt"), "root bytes");
await chmod(root, 0o100);
try {
const result = await importTree(root);
expect(result).toMatchObject({
ok: false,
error: {
code: "io",
message: expect.stringContaining("EACCES"),
partial: { rootFailure: { code: "io" } },
},
});
if (!result.ok) expect(result.error.message).toContain(root);
} finally {
await chmod(root, 0o700);
}
const recovered = await importTree(root);
expect(recovered.ok && recovered.value.entries).toContainEqual(
expect.objectContaining({ path: "main.txt", content_state: "hashed" }),
);
},
);
it.skipIf(permissionChecksUnavailable).each(["blocked", "src/blocked"])(
"preserves a partial graph when %s cannot be enumerated",
async (blocked) => {