paperlint 13.0.0 → 13.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/git/index.js +46 -18
- package/dist/adapters/git/index.js.map +1 -1
- package/docs/rules/repro/outputs-before-decision.md +4 -1
- package/package.json +1 -1
- package/src/adapters/git/history.test.ts +63 -0
- package/src/adapters/git/index.test.ts +14 -5
- package/src/adapters/git/index.ts +54 -24
|
@@ -78,9 +78,32 @@ const inHead = (git, cwd, file) => {
|
|
|
78
78
|
const t = git(cwd, ["ls-tree", "--name-only", "HEAD", "--", file]);
|
|
79
79
|
return "why" in t ? t : { present: t.out.trim() !== "" };
|
|
80
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* Every ancestor, not git's simplified history: a merge whose first parent already holds the same tree
|
|
83
|
+
* (a squash of the branch it merges) would otherwise hide that branch, and with it the commits that
|
|
84
|
+
* show the decision came first.
|
|
85
|
+
*/
|
|
86
|
+
const WHOLE_HISTORY = ["--full-history"];
|
|
87
|
+
/** Every path ever under the study's folder, oldest commit first, each commit's paths after its sha and date. */
|
|
88
|
+
const FIRST_COMMITS = [
|
|
89
|
+
"log",
|
|
90
|
+
"--reverse",
|
|
91
|
+
...WHOLE_HISTORY,
|
|
92
|
+
"--format=%x01%H%x09%cI",
|
|
93
|
+
"--name-only",
|
|
94
|
+
"--relative",
|
|
95
|
+
"--",
|
|
96
|
+
".",
|
|
97
|
+
];
|
|
81
98
|
/** The decision's first commit (null: never committed), and whether HEAD's tree still holds it. */
|
|
82
99
|
const decisionIn = (git, studyDir, decision) => {
|
|
83
|
-
const d = git(studyDir, [
|
|
100
|
+
const d = git(studyDir, [
|
|
101
|
+
"log",
|
|
102
|
+
...WHOLE_HISTORY,
|
|
103
|
+
"--format=%H%x09%cI",
|
|
104
|
+
"--",
|
|
105
|
+
decision,
|
|
106
|
+
]);
|
|
84
107
|
if ("why" in d)
|
|
85
108
|
return d;
|
|
86
109
|
const lines = d.out.trim().split("\n");
|
|
@@ -100,18 +123,10 @@ function orderIn(git, studyDir, decision) {
|
|
|
100
123
|
const d = decisionIn(git, studyDir, decision);
|
|
101
124
|
if ("why" in d)
|
|
102
125
|
return { kind: "unknown", why: d.why };
|
|
103
|
-
const files = git(studyDir,
|
|
104
|
-
"log",
|
|
105
|
-
"--reverse",
|
|
106
|
-
"--format=%x01%H%x09%cI",
|
|
107
|
-
"--name-only",
|
|
108
|
-
"--relative",
|
|
109
|
-
"--",
|
|
110
|
-
".",
|
|
111
|
-
]);
|
|
126
|
+
const files = git(studyDir, FIRST_COMMITS);
|
|
112
127
|
if ("why" in files)
|
|
113
128
|
return { kind: "unknown", why: files.why };
|
|
114
|
-
const ordered = ordersOf(git, studyDir, d.at, firstCommitsOf(files.out));
|
|
129
|
+
const ordered = ordersOf(git, studyDir, { at: d.at, path: decision }, firstCommitsOf(files.out));
|
|
115
130
|
return "why" in ordered
|
|
116
131
|
? { kind: "unknown", why: ordered.why }
|
|
117
132
|
: {
|
|
@@ -123,16 +138,29 @@ function orderIn(git, studyDir, decision) {
|
|
|
123
138
|
}
|
|
124
139
|
const isFailed = (a) => "why" in a;
|
|
125
140
|
const isOrdered = (a) => "order" in a;
|
|
126
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* The order of `commit` against the decision: after when any commit that touched the decision is its
|
|
143
|
+
* strict ancestor — not only the decision's first commit, which a squash can copy onto another line
|
|
144
|
+
* of history — the same when `commit` itself first carries it, not after otherwise.
|
|
145
|
+
*/
|
|
127
146
|
const answerFor = (git, cwd, decision, commit) => {
|
|
128
|
-
if (decision === null)
|
|
147
|
+
if (decision.at === null)
|
|
129
148
|
return { commit, order: "not-after" };
|
|
130
|
-
if (commit === decision.commit)
|
|
149
|
+
if (commit === decision.at.commit)
|
|
131
150
|
return { commit, order: "same" };
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
const r = git(cwd, [
|
|
152
|
+
"rev-list",
|
|
153
|
+
...WHOLE_HISTORY,
|
|
154
|
+
commit,
|
|
155
|
+
"--",
|
|
156
|
+
decision.path,
|
|
157
|
+
]);
|
|
158
|
+
if ("why" in r)
|
|
159
|
+
return { commit, why: r.why };
|
|
160
|
+
const touched = r.out.split("\n").filter((sha) => sha !== "");
|
|
161
|
+
if (touched.some((sha) => sha !== commit))
|
|
162
|
+
return { commit, order: "after" };
|
|
163
|
+
return { commit, order: touched.length > 0 ? "same" : "not-after" };
|
|
136
164
|
};
|
|
137
165
|
/** Each file's first commit with its order against `decision`'s; git asked once per distinct commit. */
|
|
138
166
|
function ordersOf(git, cwd, decision, firsts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/git/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/git/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,7 +60,10 @@ the same check and get the same answer. The design is
|
|
|
60
60
|
**It asks git for an order, never pins a sha**, which is why it can live as a lint rule (rules in
|
|
61
61
|
`eslint-rules/` may not run git: a pinned sha is deleted by a squash or a gc). A squash merge keeps
|
|
62
62
|
the order when the decision was merged before the run — the decide-first workflow — and a shallow
|
|
63
|
-
checkout is named, not read as a violation.
|
|
63
|
+
checkout is named, not read as a violation. A squash of a branch that held both the decision and the
|
|
64
|
+
run puts them in one commit, and that order is gone from the squashed history; merge such a branch
|
|
65
|
+
with a merge commit instead. History is read whole (`--full-history`): a merge whose first parent is
|
|
66
|
+
already identical to it (a squash of the branch it merges back) does not hide that branch's commits.
|
|
64
67
|
|
|
65
68
|
## Examples
|
|
66
69
|
|
package/package.json
CHANGED
|
@@ -17,6 +17,8 @@ const ports = gitHistory(spawnProcess(), { PATH: String(process.env.PATH) });
|
|
|
17
17
|
const T1 = "2026-10-16T10:00:00+05:00";
|
|
18
18
|
const T2 = "2026-10-16T11:00:00+05:00";
|
|
19
19
|
const T3 = "2026-10-16T12:00:00+05:00";
|
|
20
|
+
const T4 = "2026-10-16T13:00:00+05:00";
|
|
21
|
+
const T5 = "2026-10-16T14:00:00+05:00";
|
|
20
22
|
|
|
21
23
|
it("a decision committed, then changed, then never committed: three states", () => {
|
|
22
24
|
const repo = join(root, "committed");
|
|
@@ -75,6 +77,67 @@ it("outputs after the decision, in its commit, before it, and deleted since: eac
|
|
|
75
77
|
});
|
|
76
78
|
});
|
|
77
79
|
|
|
80
|
+
it("a squash on the first parent does not hide the order its merged-in branch recorded", () => {
|
|
81
|
+
const repo = join(root, "squashed");
|
|
82
|
+
initRepo(repo);
|
|
83
|
+
commit(repo, { "README.md": "r\n" }, T1);
|
|
84
|
+
git(repo, ["checkout", "-q", "-b", "study"], T1);
|
|
85
|
+
const decided = commit(repo, { "p/repro/S/decision.md": "d\n" }, T2);
|
|
86
|
+
const ran = commit(repo, { "p/repro/S/out.csv": "1\n" }, T3);
|
|
87
|
+
git(repo, ["checkout", "-q", "main"], T3);
|
|
88
|
+
commit(
|
|
89
|
+
repo,
|
|
90
|
+
{ "p/repro/S/decision.md": "d\n", "p/repro/S/out.csv": "1\n" },
|
|
91
|
+
T4,
|
|
92
|
+
"squash",
|
|
93
|
+
);
|
|
94
|
+
git(repo, ["merge", "-q", "--no-ff", "-m", "merge", "study"], T5);
|
|
95
|
+
// Guards: the merge is TREESAME to the squash, its first parent, so git's default history
|
|
96
|
+
// simplification walks only the squash and reads decision and output as born in one commit.
|
|
97
|
+
expect(
|
|
98
|
+
ports.order(
|
|
99
|
+
absolutePath(join(repo, "p/repro/S")),
|
|
100
|
+
absolutePath(join(repo, "p/repro/S/decision.md")),
|
|
101
|
+
),
|
|
102
|
+
).toEqual({
|
|
103
|
+
kind: "read",
|
|
104
|
+
decision: { commit: decided, date: T2 },
|
|
105
|
+
decisionDeleted: false,
|
|
106
|
+
files: [
|
|
107
|
+
{ path: "decision.md", commit: decided, date: T2, order: "same" },
|
|
108
|
+
{ path: "out.csv", commit: ran, date: T3, order: "after" },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("a decision squashed into main, then its branch merged back: outputs on main stay after it", () => {
|
|
114
|
+
const repo = join(root, "decision-squashed");
|
|
115
|
+
initRepo(repo);
|
|
116
|
+
commit(repo, { "README.md": "r\n" }, T1);
|
|
117
|
+
git(repo, ["checkout", "-q", "-b", "decide"], T1);
|
|
118
|
+
const original = commit(repo, { "p/repro/S/decision.md": "d\n" }, T2);
|
|
119
|
+
git(repo, ["checkout", "-q", "main"], T2);
|
|
120
|
+
commit(repo, { "p/repro/S/decision.md": "d\n" }, T3, "squash");
|
|
121
|
+
const ran = commit(repo, { "p/repro/S/out.csv": "1\n" }, T4);
|
|
122
|
+
git(repo, ["merge", "-q", "--no-ff", "-m", "merge", "decide"], T5);
|
|
123
|
+
// Guards: the earliest decision commit is on the merged-back branch and is no ancestor of the run,
|
|
124
|
+
// but the squash that carried the same decision is; any commit holding the decision before counts.
|
|
125
|
+
expect(
|
|
126
|
+
ports.order(
|
|
127
|
+
absolutePath(join(repo, "p/repro/S")),
|
|
128
|
+
absolutePath(join(repo, "p/repro/S/decision.md")),
|
|
129
|
+
),
|
|
130
|
+
).toEqual({
|
|
131
|
+
kind: "read",
|
|
132
|
+
decision: { commit: original, date: T2 },
|
|
133
|
+
decisionDeleted: false,
|
|
134
|
+
files: [
|
|
135
|
+
{ path: "decision.md", commit: original, date: T2, order: "same" },
|
|
136
|
+
{ path: "out.csv", commit: ran, date: T4, order: "after" },
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
78
141
|
it("a shallow clone is named, not judged", () => {
|
|
79
142
|
const repo = join(root, "deep");
|
|
80
143
|
initRepo(repo);
|
|
@@ -15,6 +15,7 @@ const STUDY = absolutePath("/repo/papers/p/repro/S");
|
|
|
15
15
|
const D = "d".repeat(40);
|
|
16
16
|
const A = "a".repeat(40);
|
|
17
17
|
const B = "b".repeat(40);
|
|
18
|
+
const C = "c".repeat(40);
|
|
18
19
|
const DATE = "2026-10-16T15:23:00+05:00";
|
|
19
20
|
|
|
20
21
|
/** HEAD's tree holds the decision, unless a test says otherwise. */
|
|
@@ -146,8 +147,15 @@ it("order: a shallow clone is said, not judged", () => {
|
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
it("order: each file's first commit against the decision's — after, the same, not after", () => {
|
|
149
|
-
const log = `\x01${D}\t${DATE}\nx.csv\n\x01${A}\t${DATE}\na.csv\nx.csv\n\x01${B}\t${DATE}\nb.csv\n`;
|
|
150
|
-
//
|
|
150
|
+
const log = `\x01${D}\t${DATE}\nx.csv\n\x01${A}\t${DATE}\na.csv\nx.csv\n\x01${B}\t${DATE}\nb.csv\n\x01${C}\t${DATE}\nc.csv\n`;
|
|
151
|
+
// The commits that touched the decision and are reachable from each one, as `rev-list` names them.
|
|
152
|
+
const touching: Readonly<Record<string, string>> = {
|
|
153
|
+
[A]: `${D}\n`,
|
|
154
|
+
[B]: "",
|
|
155
|
+
[C]: `${C}\n`,
|
|
156
|
+
};
|
|
157
|
+
// Guards: the decision's own commit is `same`, and so is another that first carries it (a squash);
|
|
158
|
+
// a commit with a decision commit among its strict ancestors is `after`; the rest are `not-after`.
|
|
151
159
|
const result = gitHistory(
|
|
152
160
|
scriptedProcess((c) => {
|
|
153
161
|
const sub = String(c.args[2]);
|
|
@@ -155,7 +163,7 @@ it("order: each file's first commit against the decision's — after, the same,
|
|
|
155
163
|
if (sub === "ls-tree") return IN_HEAD;
|
|
156
164
|
if (sub === "log" && c.args.includes("--reverse")) return exitedWith(log);
|
|
157
165
|
if (sub === "log") return exitedWith(`${A}\t${DATE}\n${D}\t${DATE}\n`);
|
|
158
|
-
return exitedWith(
|
|
166
|
+
return exitedWith(touching[String(c.args[4])] ?? "");
|
|
159
167
|
}),
|
|
160
168
|
{},
|
|
161
169
|
).order(STUDY, FILE);
|
|
@@ -167,13 +175,14 @@ it("order: each file's first commit against the decision's — after, the same,
|
|
|
167
175
|
{ path: "x.csv", commit: D, date: DATE, order: "same" },
|
|
168
176
|
{ path: "a.csv", commit: A, date: DATE, order: "after" },
|
|
169
177
|
{ path: "b.csv", commit: B, date: DATE, order: "not-after" },
|
|
178
|
+
{ path: "c.csv", commit: C, date: DATE, order: "same" },
|
|
170
179
|
],
|
|
171
180
|
});
|
|
172
181
|
});
|
|
173
182
|
|
|
174
183
|
it("order: no decision commit makes every file not-after; a failed ancestry question is a reason", () => {
|
|
175
184
|
const log = `\x01${A}\t${DATE}\na.csv\n`;
|
|
176
|
-
const answer = (
|
|
185
|
+
const answer = (revList: ProcessExit, decisionLog: string) =>
|
|
177
186
|
gitHistory(
|
|
178
187
|
scriptedProcess((c) => {
|
|
179
188
|
const sub = String(c.args[2]);
|
|
@@ -182,7 +191,7 @@ it("order: no decision commit makes every file not-after; a failed ancestry ques
|
|
|
182
191
|
if (sub === "log" && c.args.includes("--reverse"))
|
|
183
192
|
return exitedWith(log);
|
|
184
193
|
if (sub === "log") return exitedWith(decisionLog);
|
|
185
|
-
return
|
|
194
|
+
return revList;
|
|
186
195
|
}),
|
|
187
196
|
{},
|
|
188
197
|
).order(STUDY, FILE);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (`git status --porcelain`)
|
|
7
7
|
* order whether the clone is shallow; the decision file's first commit; every file's first
|
|
8
8
|
* commit under the study's folder (`git log --reverse --name-only`); and for each, whether
|
|
9
|
-
* the decision
|
|
9
|
+
* any commit that touched the decision is its strict ancestor (`git rev-list -- <decision>`)
|
|
10
10
|
*/
|
|
11
11
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
12
12
|
import type {
|
|
@@ -125,6 +125,25 @@ const inHead = (
|
|
|
125
125
|
return "why" in t ? t : { present: t.out.trim() !== "" };
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Every ancestor, not git's simplified history: a merge whose first parent already holds the same tree
|
|
130
|
+
* (a squash of the branch it merges) would otherwise hide that branch, and with it the commits that
|
|
131
|
+
* show the decision came first.
|
|
132
|
+
*/
|
|
133
|
+
const WHOLE_HISTORY = ["--full-history"] as const;
|
|
134
|
+
|
|
135
|
+
/** Every path ever under the study's folder, oldest commit first, each commit's paths after its sha and date. */
|
|
136
|
+
const FIRST_COMMITS = [
|
|
137
|
+
"log",
|
|
138
|
+
"--reverse",
|
|
139
|
+
...WHOLE_HISTORY,
|
|
140
|
+
"--format=%x01%H%x09%cI",
|
|
141
|
+
"--name-only",
|
|
142
|
+
"--relative",
|
|
143
|
+
"--",
|
|
144
|
+
".",
|
|
145
|
+
] as const;
|
|
146
|
+
|
|
128
147
|
/** The decision's first commit (null: never committed), and whether HEAD's tree still holds it. */
|
|
129
148
|
const decisionIn = (
|
|
130
149
|
git: Git,
|
|
@@ -133,7 +152,13 @@ const decisionIn = (
|
|
|
133
152
|
):
|
|
134
153
|
| { readonly at: CommitAt | null; readonly deleted: boolean }
|
|
135
154
|
| { readonly why: string } => {
|
|
136
|
-
const d = git(studyDir, [
|
|
155
|
+
const d = git(studyDir, [
|
|
156
|
+
"log",
|
|
157
|
+
...WHOLE_HISTORY,
|
|
158
|
+
"--format=%H%x09%cI",
|
|
159
|
+
"--",
|
|
160
|
+
decision,
|
|
161
|
+
]);
|
|
137
162
|
if ("why" in d) return d;
|
|
138
163
|
const lines = d.out.trim().split("\n");
|
|
139
164
|
const first = commitAt(nth(lines, lines.length - 1));
|
|
@@ -153,17 +178,14 @@ function orderIn(
|
|
|
153
178
|
if (shallow.out.trim() === "true") return { kind: "shallow" };
|
|
154
179
|
const d = decisionIn(git, studyDir, decision);
|
|
155
180
|
if ("why" in d) return { kind: "unknown", why: d.why };
|
|
156
|
-
const files = git(studyDir,
|
|
157
|
-
"log",
|
|
158
|
-
"--reverse",
|
|
159
|
-
"--format=%x01%H%x09%cI",
|
|
160
|
-
"--name-only",
|
|
161
|
-
"--relative",
|
|
162
|
-
"--",
|
|
163
|
-
".",
|
|
164
|
-
]);
|
|
181
|
+
const files = git(studyDir, FIRST_COMMITS);
|
|
165
182
|
if ("why" in files) return { kind: "unknown", why: files.why };
|
|
166
|
-
const ordered = ordersOf(
|
|
183
|
+
const ordered = ordersOf(
|
|
184
|
+
git,
|
|
185
|
+
studyDir,
|
|
186
|
+
{ at: d.at, path: decision },
|
|
187
|
+
firstCommitsOf(files.out),
|
|
188
|
+
);
|
|
167
189
|
return "why" in ordered
|
|
168
190
|
? { kind: "unknown", why: ordered.why }
|
|
169
191
|
: {
|
|
@@ -185,29 +207,37 @@ const isOrdered = (
|
|
|
185
207
|
a: Answer,
|
|
186
208
|
): a is { readonly commit: string; readonly order: Order } => "order" in a;
|
|
187
209
|
|
|
188
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* The order of `commit` against the decision: after when any commit that touched the decision is its
|
|
212
|
+
* strict ancestor — not only the decision's first commit, which a squash can copy onto another line
|
|
213
|
+
* of history — the same when `commit` itself first carries it, not after otherwise.
|
|
214
|
+
*/
|
|
189
215
|
const answerFor = (
|
|
190
216
|
git: Git,
|
|
191
217
|
cwd: AbsolutePath,
|
|
192
|
-
decision: CommitAt | null,
|
|
218
|
+
decision: { readonly at: CommitAt | null; readonly path: AbsolutePath },
|
|
193
219
|
commit: string,
|
|
194
220
|
): Answer => {
|
|
195
|
-
if (decision === null) return { commit, order: "not-after" };
|
|
196
|
-
if (commit === decision.commit) return { commit, order: "same" };
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
if (decision.at === null) return { commit, order: "not-after" };
|
|
222
|
+
if (commit === decision.at.commit) return { commit, order: "same" };
|
|
223
|
+
const r = git(cwd, [
|
|
224
|
+
"rev-list",
|
|
225
|
+
...WHOLE_HISTORY,
|
|
226
|
+
commit,
|
|
227
|
+
"--",
|
|
228
|
+
decision.path,
|
|
229
|
+
]);
|
|
230
|
+
if ("why" in r) return { commit, why: r.why };
|
|
231
|
+
const touched = r.out.split("\n").filter((sha) => sha !== "");
|
|
232
|
+
if (touched.some((sha) => sha !== commit)) return { commit, order: "after" };
|
|
233
|
+
return { commit, order: touched.length > 0 ? "same" : "not-after" };
|
|
204
234
|
};
|
|
205
235
|
|
|
206
236
|
/** Each file's first commit with its order against `decision`'s; git asked once per distinct commit. */
|
|
207
237
|
function ordersOf(
|
|
208
238
|
git: Git,
|
|
209
239
|
cwd: AbsolutePath,
|
|
210
|
-
decision: CommitAt | null,
|
|
240
|
+
decision: { readonly at: CommitAt | null; readonly path: AbsolutePath },
|
|
211
241
|
firsts: ReadonlyMap<string, CommitAt>,
|
|
212
242
|
): { readonly files: readonly FirstCommit[] } | { readonly why: string } {
|
|
213
243
|
const answers = [...new Set([...firsts.values()].map((c) => c.commit))].map(
|