Public: anyone can read this; the crew can change it.

A script names the files on main that grew by pure appends in more than 10 commits over the last 7 days #6511

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6499-a-script-names-the-files-on-main-that-gr → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/6499-a-script-names-the-files-on-main-that-gr was merged into main as 22a9357.

Review

Sign in to review this change.
2 files changed +126 −0
scripts/append-growth.ts added +77
@@ -0,0 +1,77 @@
1 /**
2 * The files that grow by pure appends: `bun scripts/append-growth.ts
3 * [--days 7] [--min 10]` reads `git log --numstat` on the current branch and
4 * prints, worst first, each file whose commits in the window only added
5 * lines (none deleted) more than `--min` times. Append-only registries are
6 * where pulls collide; each should become one file per entry.
7 */
8
9 export type Row = { commit: string; added: number; deleted: number; path: string };
10 export type Growth = { path: string; count: number };
11
12 /** Parse `git log --numstat --format=%x00%H` output; binary rows are skipped. */
13 export function parseNumstat(text: string): Row[] {
14 const rows: Row[] = [];
15 for (const block of text.split("\0")) {
16 const [commit, ...lines] = block.split("\n");
17 if (!commit) continue;
18 for (const line of lines) {
19 const m = /^(\d+)\t(\d+)\t(.+)$/.exec(line);
20 if (m) rows.push({ commit, added: Number(m[1]), deleted: Number(m[2]), path: m[3]! });
21 }
22 }
23 return rows;
24 }
25
26 /** Files every touching commit only added to, in more than `min` commits, worst first. */
27 export function appendGrowth(rows: readonly Row[], min: number): Growth[] {
28 const seen = new Map<string, { commits: Set<string>; pure: boolean }>();
29 for (const row of rows) {
30 const file = seen.get(row.path) ?? { commits: new Set(), pure: true };
31 if (row.deleted > 0) file.pure = false;

Non-blocking: under the strict reading, one deleting commit in the window takes a file off the list. So a registry that gets one reorder or rename a week stops showing, even though it is still a collision point. If the #5930 follow-up misses files it expected, an --allow-edits N tolerance would be a small change here.

32 else if (row.added > 0) file.commits.add(row.commit);
33 seen.set(row.path, file);
34 }
35 return [...seen]
36 .filter(([, f]) => f.pure && f.commits.size > min)
37 .map(([path, f]) => ({ path, count: f.commits.size }))
38 .sort((a, b) => b.count - a.count || a.path.localeCompare(b.path));
39 }
40
41 /** A non-negative integer from an argument, or null. */
42 export function integer(value: string | undefined): number | null {
43 return value !== undefined && /^\d{1,6}$/.test(value) ? Number(value) : null;
44 }
45
46 export function parseArgs(argv: readonly string[]): { days: number; min: number } | null {
47 let days = 7;
48 let min = 10;
49 for (let i = 0; i < argv.length; i += 2) {
50 const n = integer(argv[i + 1]);
51 if (n === null) return null;
52 if (argv[i] === "--days") days = n;
53 else if (argv[i] === "--min") min = n;
54 else return null;
55 }
56 return { days, min };
57 }
58
59 if (import.meta.main) {
60 const args = parseArgs(process.argv.slice(2));
61 if (!args) {
62 console.error("usage: bun scripts/append-growth.ts [--days N] [--min N] (integers)");
63 process.exit(2);
64 }
65 const git = Bun.spawnSync(
66 ["git", "log", "--numstat", "--no-renames", "--format=%x00%H", `--since=${args.days}.days.ago`],

Non-blocking: git quotes paths that have non-ASCII bytes or special characters ("src/caf\303\251.ts") unless core.quotePath is off. Those rows would show up quoted. Passing -c core.quotePath=false before log, or using -z, keeps paths as they are.

67 { cwd: new URL("..", import.meta.url).pathname, maxBuffer: 1 << 28 },
68 );
69 if (git.exitCode !== 0) {
70 console.error(git.stderr.toString());
71 process.exit(1);
72 }
73 const found = appendGrowth(parseNumstat(git.stdout.toString()), args.min);
74 for (const f of found) console.log(`${f.count}\t${f.path}`);
75 if (found.length === 0)
76 console.log(`no file grew by pure appends in more than ${args.min} commits in ${args.days} days`);
77 }
test/append-growth.test.ts added +49
@@ -0,0 +1,49 @@
1 /** `scripts/append-growth.ts`: the counting is pure over numstat rows. */
2
3 import { expect, test } from "bun:test";
4 import { appendGrowth, integer, parseArgs, parseNumstat } from "../scripts/append-growth.ts";
5
6 const row = (commit: string, added: number, deleted: number, path: string) => ({
7 commit,
8 added,
9 deleted,
10 path,
11 });
12
13 test("counts pure-append commits per file, worst first, above the minimum", () => {
14 const rows = [
15 ...["a", "b", "c"].map((c) => row(c, 1, 0, "reg.ts")),
16 ...["a", "b", "c", "d"].map((c) => row(c, 2, 0, "list.ts")),
17 row("a", 1, 0, "once.ts"),
18 ];
19 expect(appendGrowth(rows, 2)).toEqual([
20 { path: "list.ts", count: 4 },
21 { path: "reg.ts", count: 3 },
22 ]);
23 expect(appendGrowth(rows, 3)).toEqual([{ path: "list.ts", count: 4 }]);
24 });
25
26 test("a file with any deleting commit is not append-only", () => {
27 const rows = [row("a", 1, 0, "f"), row("b", 1, 0, "f"), row("c", 1, 1, "f")];
28 expect(appendGrowth(rows, 0)).toEqual([]);
29 });
30
31 test("parses numstat, skipping binary rows", () => {
32 const text = "\0abc\n\n1\t0\ta.ts\n-\t-\timg.png\n3\t2\tb/c.ts\n\0def\n\n4\t0\ta.ts\n";
33 expect(parseNumstat(text)).toEqual([
34 row("abc", 1, 0, "a.ts"),
35 row("abc", 3, 2, "b/c.ts"),
36 row("def", 4, 0, "a.ts"),
37 ]);
38 });
39
40 test("arguments are validated integers", () => {
41 expect(parseArgs([])).toEqual({ days: 7, min: 10 });
42 expect(parseArgs(["--days", "3", "--min", "5"])).toEqual({ days: 3, min: 5 });
43 expect(parseArgs(["--days", "x; rm -rf /"])).toBeNull();
44 expect(parseArgs(["--days", "-1"])).toBeNull();
45 expect(parseArgs(["--days"])).toBeNull();
46 expect(parseArgs(["--other", "1"])).toBeNull();
47 expect(integer("12")).toBe(12);
48 expect(integer("1.5")).toBeNull();
49 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs