Skip to content
Source
Sign inSign up

gitIndexStat.test.ts

130 lines5.8 KB

1// @effect-diagnostics nodeBuiltinImport:off - Fixtures drive git and cp directly and read raw stat data.
2import * as NodeChildProcess from "node:child_process";
3import * as NodeFS from "node:fs";
4import * as NodeOS from "node:os";
5import * as NodePath from "node:path";
6​
7import { assert, describe, it } from "@effect/vitest";
8import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
9import * as Effect from "effect/Effect";
10​
11import { indexEntryStat, parseIndex, restatIndex } from "./gitIndexStat.ts";
12​
13const git = (cwd: string, args: ReadonlyArray<string>, env: NodeJS.ProcessEnv = {}) =>
14 NodeChildProcess.execFileSync("git", args, {
15 cwd,
16 env: { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1", ...env },
17 encoding: "utf8",
18 });
19​
20// A repository whose checkout in `source` was written by Git with its own index, then copied
21// to `copy` the way a clone would: same contents and mtimes, new inodes and ctimes.
22const makeCopiedCheckout = Effect.acquireRelease(
23 Effect.sync(() => {
24 const root = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "git-index-stat-"));
25 const repo = NodePath.join(root, "repo");
26 NodeFS.mkdirSync(repo);
27 git(repo, ["init", "-q"]);
28 git(repo, ["config", "filter.probe.clean", "echo read >> ../clean-runs; cat"]);
29 NodeFS.writeFileSync(NodePath.join(repo, ".gitattributes"), "*.txt filter=probe\n");
30 NodeFS.mkdirSync(NodePath.join(repo, "nested"));
31 NodeFS.writeFileSync(NodePath.join(repo, "a.txt"), "alpha\n");
32 NodeFS.writeFileSync(NodePath.join(repo, "nested", "b.txt"), "beta\n");
33 NodeFS.writeFileSync(NodePath.join(repo, "run.sh"), "#!/bin/sh\n", { mode: 0o755 });
34 NodeFS.symlinkSync("a.txt", NodePath.join(repo, "link"));
35 git(repo, ["add", "."]);
36 git(repo, ["-c", "user.name=T", "-c", "user.email=t@t", "commit", "-qm", "init"]);
37 const tree = git(repo, ["rev-parse", "HEAD^{tree}"]).trim();
38 const source = NodePath.join(root, "source");
39 const sourceIndex = NodePath.join(root, "source.index");
40 NodeFS.mkdirSync(source);
41 const env = { GIT_INDEX_FILE: sourceIndex };
42 git(
43 repo,
44 ["-c", "index.version=2", `--work-tree=${source}`, "read-tree", "--reset", "-u", tree],
45 env,
46 );
47 // Old mtimes keep every entry clear of Git's racy-clean re-read.
48 for (const name of ["a.txt", "nested/b.txt", "run.sh", ".gitattributes"]) {
49 NodeFS.utimesSync(NodePath.join(source, name), 1_000_000, 1_000_000);
50 }
51 git(
52 repo,
53 ["-c", "index.version=2", `--work-tree=${source}`, "update-index", "-q", "--refresh"],
54 env,
55 );
56 const copy = NodePath.join(root, "copy");
57 NodeChildProcess.execFileSync("cp", ["-a", source, copy]);
58 // Setup ran the filter itself; only reads after this point count.
59 NodeFS.rmSync(NodePath.join(root, "clean-runs"), { force: true });
60 return { root, repo, source, sourceIndex, copy };
61 }),
62 ({ root }) => Effect.sync(() => NodeFS.rmSync(root, { recursive: true, force: true })),
63);
64​
65const statAll = (root: string, paths: ReadonlyArray<string>) =>
66 paths.map((entryPath) => {
67 try {
68 return indexEntryStat(NodeFS.lstatSync(NodePath.join(root, entryPath), { bigint: true }));
69 } catch {
70 return undefined;
71 }
72 });
73​
74// The feature is macOS-only; the fixtures need symlinks.
75describe.skipIf(!symlinksSupported)("restatIndex", () => {
76 it.effect("makes Git trust copied files without reading them", () =>
77 Effect.gen(function* () {
78 const { root, repo, sourceIndex, copy } = yield* makeCopiedCheckout;
79 const parsed = parseIndex(NodeFS.readFileSync(sourceIndex));
80 assert.isNotNull(parsed);
81 const paths = parsed!.entries.map((entry) => entry.path);
82 assert.deepStrictEqual(paths, [".gitattributes", "a.txt", "link", "nested/b.txt", "run.sh"]);
83​
84 const restated = restatIndex(parsed!, statAll(copy, paths), { verify: true });
85 assert.isNotNull(restated);
86 const copyIndex = NodePath.join(root, "copy.index");
87 NodeFS.writeFileSync(copyIndex, restated!);
88 const status = git(
89 repo,
90 [`--work-tree=${copy}`, "status", "--porcelain", "--untracked-files=no"],
91 {
92 GIT_INDEX_FILE: copyIndex,
93 },
94 );
95 assert.equal(status, "");
96 assert.isFalse(NodeFS.existsSync(NodePath.join(root, "clean-runs")));
97 }),
98 );
99​
100 it.effect("rejects a copy whose file no longer matches what Git recorded", () =>
101 Effect.gen(function* () {
102 const { sourceIndex, copy } = yield* makeCopiedCheckout;
103 const parsed = parseIndex(NodeFS.readFileSync(sourceIndex))!;
104 const paths = parsed.entries.map((entry) => entry.path);
105 NodeFS.writeFileSync(NodePath.join(copy, "a.txt"), "ALPHA\n");
106 assert.isNull(restatIndex(parsed, statAll(copy, paths), { verify: true }));
107 assert.isNotNull(restatIndex(parsed, statAll(copy, paths), { verify: false }));
108 }),
109 );
110​
111 it.effect("rejects two tracked paths that are one file, as on a case-insensitive volume", () =>
112 Effect.gen(function* () {
113 const { sourceIndex, copy } = yield* makeCopiedCheckout;
114 const parsed = parseIndex(NodeFS.readFileSync(sourceIndex))!;
115 const paths = parsed.entries.map((entry) => entry.path);
116 NodeFS.rmSync(NodePath.join(copy, "nested", "b.txt"));
117 NodeFS.linkSync(NodePath.join(copy, "a.txt"), NodePath.join(copy, "nested", "b.txt"));
118 assert.isNull(restatIndex(parsed, statAll(copy, paths), { verify: false }));
119 }),
120 );
121​
122 it.effect("leaves indexes it cannot rewrite safely to Git", () =>
123 Effect.gen(function* () {
124 const { repo, source, sourceIndex } = yield* makeCopiedCheckout;
125 const env = { GIT_INDEX_FILE: sourceIndex };
126 git(repo, [`--work-tree=${source}`, "update-index", "--index-version", "4"], env);
127 assert.isNull(parseIndex(NodeFS.readFileSync(sourceIndex)));
128 }),
129 );
130});