Split The code is public; the app answers only to the crew.
The tree guard is red on any machine that has run the app: it fails on files .gitignore already excuses #162
Check · Review · Land · Live done — Landed and live in production.
scout/cs:scout/129-the-tree-guard-is-red-on-any-machine-tha → main 1 commit fork
1 file
changed +21
−6
src/tree.test.ts +21 −6
| @@ -6,6 +6,19 @@ | ||
| 6 | 6 | * `git ls-files` isn't available to a test — and fails the same way CI |
| 7 | 7 | * would if either shape came back: a Netscape cookie jar, or a directory |
| 8 | 8 | * named for one of the scratch patterns `.gitignore` already lists. |
| 9 | * | |
| 10 | * "Ignored" and "committed" are different questions. A file that matches | |
| 11 | * one of `.gitignore`'s own patterns (`data/app.db`, `static/app.*.js`) | |
| 12 | * is proof the ignore is doing its job, not a violation — the app writes | |
| 13 | * those on every run and every build, so flagging them makes the guard | |
| 14 | * red on any machine that has used the app at all (issue #129). A | |
| 15 | * directory named for one of `.gitignore`'s scratch entries is different: | |
| 16 | * nothing normal creates `.scout-tmp/` or `.checkdata/`, so seeing one | |
| 17 | * still means someone put it there on purpose, ignored or not — the | |
| 18 | * ignore stops it from being committed, not from existing or from being | |
| 19 | * force-added, so this still has to look. A cookie jar is checked | |
| 20 | * regardless of either answer: ignoring a path doesn't undo a commit | |
| 21 | * that already happened, so a cookie jar is a credential wherever it sits. | |
| 9 | 22 | */ |
| 10 | 23 | |
| 11 | 24 | import { expect, test } from "bun:test"; |
| @@ -25,9 +38,6 @@ const lines = gitignore | ||
| 25 | 38 | const scratchDirNames = new Set( |
| 26 | 39 | lines.filter((line) => line.endsWith("/")).map((line) => line.slice(0, -1)), |
| 27 | 40 | ); |
| 28 | const filePatterns = lines | |
| 29 | .filter((line) => !line.endsWith("/") && line.includes("*")) | |
| 30 | .map((line) => new RegExp(`^${line.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`)); | |
| 31 | 41 | |
| 32 | 42 | /** Just enough of the file to know its first line, not the whole thing. */ |
| 33 | 43 | const firstLine = (path: string): string => { |
| @@ -52,9 +62,14 @@ const walk = (dir: string): void => { | ||
| 52 | 62 | if (scratchDirNames.has(entry.name)) violations.push(`${relative}/ is a scratch directory .gitignore already refuses`); |
| 53 | 63 | walk(path); |
| 54 | 64 | } else if (entry.isFile()) { |
| 55 | if (filePatterns.some((pattern) => pattern.test(entry.name))) { | |
| 56 | violations.push(`${relative} matches a scratch pattern .gitignore already refuses`); | |
| 57 | } else if (firstLine(path).trim() === COOKIE_JAR_HEADER) { | |
| 65 | // A file's name is not checked against .gitignore's own scratch | |
| 66 | // patterns (`*.db`, `static/app.*.js`): matching one of those means | |
| 67 | // the ignore is working, and running the app or building the bundle | |
| 68 | // is exactly what produces them, on every machine, every time — the | |
| 69 | // opposite of a violation. Its content still is: a cookie jar's | |
| 70 | // header doesn't stop being a credential because its filename | |
| 71 | // happens to be ignored. | |
| 72 | if (firstLine(path).trim() === COOKIE_JAR_HEADER) { | |
| 58 | 73 | violations.push(`${relative} is a Netscape cookie jar`); |
| 59 | 74 | } |
| 60 | 75 | } |
new above
offline