fix(reference): Exclude Git pointer files from source identities (#7667)
* fix(reference): Exclude Git pointer files from source identities Match .git pointer files in the default import policy after the existing directory rules, preserving ordinary-checkout exclusion provenance and Git HEAD discovery. Cover linked-worktree identities, repeated imports, nested metadata, similarly named source files, and configured-secret precedence. Independent review: parent independent review APPROVE for Draft CI on patch 61bddf108952c77858fa71c333b4bb8a05ab33e8b03f5c6bccd879cd19c7c379 and source tree2d2372a9ec. Review triggers: delegated shipping output, source inventory/file I/O behavior, and more than 50 lines. Validation: npm run check passed with Node 24.22.1/npm 11.16.0 on Windows; tracked Vitest regression 1 passed/3 Windows skips; test discovery passed. Eight offline Linux production imports passed on cached Node 22.19.0 as supporting evidence only. Pinned Linux/macOS Vitest cases await hosted CI. Fixes #7551 * test(reference): Isolate worktree fixtures from host Git settings Route every fixture Git command through one wrapper with a filtered child environment, owned empty global config/template/hook paths, and no system config reads. Keep resource cleanup under the existing temporary owner. These settings are scoped to synthetic child processes; host Git settings and this repository's commit/push checks are unchanged. Targeted independent review: parent review APPROVE on patch 4be9a2af3e40edac3d64f687170e966a87fcf762f1877f2c55016e120bcd90ac and treea2ba91ee9c. The reviewer confirmed the native proof helper hash, complete fixture command coverage, and owned cleanup. Review triggers: delegated follow-up and fixture file/process boundaries. Validation: npm run check:fast passed; targeted Vitest 1 passed/3 native Windows skips. On pinned Node 24.22.1, the actual helper rejects inherited hooks/templates while commit, worktree creation, HEAD and status checks pass. The unisolated control copies the injected template and its commit fails. Refs #7551
This commit is contained in:
1 parent
f5cfe09c07
commit
e01fcfb4b2
2 files changed
+210
No files matched your search
@@ -125,6 +125,8 @@ export interface ReferenceSourceImportOptions {
|
||||
export const DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS = [
|
||||
".git/",
|
||||
".git/hooks/",
|
||||
// Match pointer files after directory rules to preserve their provenance.
|
||||
".git",
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"build/",
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import ignore from "ignore";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS } from "../../../src/application/ReferenceSourceImportTypes.js";
|
||||
import { createHistoricalSourceManifest } from "../../../src/domain/referenceSourceGraph.js";
|
||||
import { execFileOutput } from "../../../src/process/ExecFileOutput.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { importReferenceSource } from "../../support/referenceSourceResourceScope.js";
|
||||
|
||||
const importTree = async (
|
||||
root: string,
|
||||
secretPatterns: readonly string[] = [],
|
||||
excludePaths: readonly string[] = [],
|
||||
) => {
|
||||
const result = await importReferenceSource({
|
||||
root,
|
||||
caller: "git-metadata-test",
|
||||
policy: { secretPatterns },
|
||||
excludePaths,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
};
|
||||
|
||||
const fixtureGit = (
|
||||
directory: string,
|
||||
configuration: string,
|
||||
args: readonly string[],
|
||||
) =>
|
||||
execFileOutput(
|
||||
"git",
|
||||
["-c", `core.hooksPath=${join(configuration, "no-hooks")}`, ...args],
|
||||
{
|
||||
cwd: directory,
|
||||
env: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
([name]) => !name.toUpperCase().startsWith("GIT_"),
|
||||
),
|
||||
),
|
||||
GIT_CONFIG_GLOBAL: join(configuration, "gitconfig"),
|
||||
GIT_CONFIG_NOSYSTEM: "1",
|
||||
GIT_TEMPLATE_DIR: join(configuration, "no-template"),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
it("excludes Git metadata files and directories without broadening directory-only defaults", () => {
|
||||
const matcher = ignore().add([...DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS]);
|
||||
for (const path of [".git", ".git/", "nested/.git", "nested/.git/"])
|
||||
expect(matcher.ignores(path), path).toBe(true);
|
||||
for (const path of [".gitignore", ".github", ".gitkeep", "dist"])
|
||||
expect(matcher.ignores(path), path).toBe(false);
|
||||
expect(matcher.ignores("dist/")).toBe(true);
|
||||
expect(matcher.test(".git").rule?.pattern).toBe(".git");
|
||||
expect(matcher.test(".git/").rule?.pattern).toBe(".git/");
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"reference source Git metadata exclusions",
|
||||
() => {
|
||||
it("keeps linked-worktree identities stable while retaining HEAD and ordinary-checkout provenance", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-git-metadata-");
|
||||
const repository = join(root, "repository");
|
||||
await Promise.all([
|
||||
mkdir(join(root, "no-hooks")),
|
||||
mkdir(join(root, "no-template")),
|
||||
writeFile(join(root, "gitconfig"), ""),
|
||||
]);
|
||||
await mkdir(join(repository, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(repository, "src", "main.js"),
|
||||
"export const value = 7;\n",
|
||||
);
|
||||
await writeFile(join(repository, ".gitignore"), "*.log\n");
|
||||
const git = (...args: string[]) => fixtureGit(repository, root, args);
|
||||
await git("init", "--initial-branch=main");
|
||||
await git("add", ".gitignore", "src/main.js");
|
||||
await git(
|
||||
"-c",
|
||||
"user.name=REA fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
"commit",
|
||||
"-m",
|
||||
"fixture",
|
||||
);
|
||||
const head = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
const leftRoot = join(root, "left");
|
||||
const rightRoot = join(root, "right");
|
||||
for (const worktree of [leftRoot, rightRoot]) {
|
||||
await git("worktree", "add", "--detach", worktree, head);
|
||||
expect(
|
||||
(await fixtureGit(worktree, root, ["status", "--porcelain"])).stdout,
|
||||
).toBe("");
|
||||
}
|
||||
expect(await readFile(join(leftRoot, ".git"), "utf8")).not.toBe(
|
||||
await readFile(join(rightRoot, ".git"), "utf8"),
|
||||
);
|
||||
const importSelectedTree = async (source: string) => {
|
||||
const graph = await importTree(source);
|
||||
expect(graph.vcs).toEqual({ kind: "git", head, dirty: null });
|
||||
expect(
|
||||
graph.entries
|
||||
.filter(({ kind }) => kind === "file")
|
||||
.map(({ path }) => path),
|
||||
).toEqual([".gitignore", "src/main.js"]);
|
||||
expect(
|
||||
graph.entries.some(
|
||||
({ path }) => path === ".git" || path.startsWith(".git/"),
|
||||
),
|
||||
).toBe(false);
|
||||
return graph;
|
||||
};
|
||||
const left = await importSelectedTree(leftRoot);
|
||||
const right = await importSelectedTree(rightRoot);
|
||||
const repeated = await importSelectedTree(leftRoot);
|
||||
expect(createHistoricalSourceManifest(left)).toEqual(
|
||||
createHistoricalSourceManifest(right),
|
||||
);
|
||||
expect(createHistoricalSourceManifest(repeated)).toEqual(
|
||||
createHistoricalSourceManifest(left),
|
||||
);
|
||||
for (const graph of [left, right])
|
||||
expect(graph.exclusions).toEqual([
|
||||
{
|
||||
path: ".git",
|
||||
entry_kind: "file",
|
||||
reason: "default-ignored",
|
||||
pattern: ".git",
|
||||
},
|
||||
]);
|
||||
const ordinary = await importSelectedTree(repository);
|
||||
expect(ordinary.exclusions).toEqual([
|
||||
{
|
||||
path: ".git",
|
||||
entry_kind: "directory",
|
||||
reason: "default-ignored",
|
||||
pattern: ".git/",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["file", "directory"] as const)(
|
||||
"prunes nested .git %s metadata and preserves configured-secret precedence",
|
||||
async (kind) => {
|
||||
const root = await createTestTempDirectory("rea-reference-nested-git-");
|
||||
const nested = join(root, "nested");
|
||||
await mkdir(nested);
|
||||
if (kind === "file")
|
||||
await writeFile(
|
||||
join(nested, ".git"),
|
||||
"gitdir: /checkout-specific/metadata\n",
|
||||
);
|
||||
else {
|
||||
await mkdir(join(nested, ".git", "hooks"), { recursive: true });
|
||||
await writeFile(
|
||||
join(nested, ".git", "hooks", "pre-commit"),
|
||||
"metadata\n",
|
||||
);
|
||||
}
|
||||
for (const name of [
|
||||
".gitignore",
|
||||
".github",
|
||||
".gitkeep",
|
||||
"dist",
|
||||
"source.js",
|
||||
])
|
||||
await writeFile(join(nested, name), "");
|
||||
for (const secretPatterns of [[], [".git"]]) {
|
||||
const graph = await importTree(
|
||||
root,
|
||||
secretPatterns,
|
||||
secretPatterns.length === 0 ? [] : ["!.git"],
|
||||
);
|
||||
expect(graph.exclusions).toEqual([
|
||||
{
|
||||
path: "nested/.git",
|
||||
entry_kind: kind,
|
||||
reason:
|
||||
secretPatterns.length === 0
|
||||
? "default-ignored"
|
||||
: "configured-secret",
|
||||
pattern:
|
||||
secretPatterns.length === 0 && kind === "directory"
|
||||
? ".git/"
|
||||
: ".git",
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
graph.entries
|
||||
.filter(({ kind }) => kind === "file")
|
||||
.map(({ path }) => path),
|
||||
).toEqual([
|
||||
"nested/.github",
|
||||
"nested/.gitignore",
|
||||
"nested/.gitkeep",
|
||||
"nested/dist",
|
||||
"nested/source.js",
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user