@jwilger/pi-development-system 0.22.0 → 0.23.0
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/package.json +1 -1
- package/src/core/exec.ts +2 -0
- package/src/gates/commit-guard.ts +47 -19
package/package.json
CHANGED
package/src/core/exec.ts
CHANGED
|
@@ -11,6 +11,8 @@ export const TIMEOUT_CODE = 124;
|
|
|
11
11
|
/**
|
|
12
12
|
* pi reports a killed command as `code: 0, killed: true`, so a timed-out `git diff` would look like an
|
|
13
13
|
* empty diff. A guard must never read that as "nothing changed": make the kill a failure.
|
|
14
|
+
* Limit: a process killed from outside (e.g. the OOM killer) also reads as `code: 0` with `killed` unset;
|
|
15
|
+
* pi's API does not expose that, so it cannot be detected here.
|
|
14
16
|
*/
|
|
15
17
|
export const timeoutAsFailure = (result: {
|
|
16
18
|
code: number;
|
|
@@ -137,40 +137,68 @@ async function reviewNeeds(
|
|
|
137
137
|
if (gap !== undefined) return [{ gate: REVIEW, why: gap }];
|
|
138
138
|
// The review saw the work tree; a plain `git commit` records the index. A file staged and then edited
|
|
139
139
|
// again would commit the older, unreviewed content.
|
|
140
|
-
const
|
|
140
|
+
const names = await changedNames(deps.exec, ctx.cwd);
|
|
141
|
+
if (names === undefined) {
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
gate: REVIEW,
|
|
145
|
+
why: "cannot read which files are staged or edited, so the staged content cannot be compared with the reviewed work tree",
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
const stale = stagedThenEdited(names, command);
|
|
141
150
|
return stale.length === 0
|
|
142
151
|
? []
|
|
143
152
|
: [
|
|
144
153
|
{
|
|
145
154
|
gate: REVIEW,
|
|
146
|
-
why: `the staged content of ${stale.join(", ")} differs from the work tree that was reviewed (edited after \`git add
|
|
155
|
+
why: `the staged content of ${stale.join(", ")} differs from the work tree that was reviewed (edited after \`git add\`, or only partly staged); stage it again, or depart if committing in parts is deliberate`,
|
|
147
156
|
},
|
|
148
157
|
];
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
/** Files staged and also changed since staging; empty when the commit stages everything itself. */
|
|
152
161
|
const stagedThenEdited = (
|
|
153
|
-
names: { staged: readonly string[]; unstaged: readonly string[] }
|
|
162
|
+
names: { staged: readonly string[]; unstaged: readonly string[] },
|
|
154
163
|
command: string,
|
|
155
|
-
): string[] =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
): string[] => {
|
|
165
|
+
const invocations = resolveGit(command).invocations;
|
|
166
|
+
if (stagesAllTracked(invocations)) return [];
|
|
167
|
+
// A path that is an argument of the commit or add itself is committed (or re-staged) from the work tree,
|
|
168
|
+
// which was reviewed. Message text is one argument and never equals a path.
|
|
169
|
+
const named = new Set(
|
|
170
|
+
invocations.filter((i) => i.sub === "commit" || i.sub === "add").flatMap((i) => i.args),
|
|
171
|
+
);
|
|
172
|
+
return names.staged.filter((name) => names.unstaged.includes(name) && !named.has(name));
|
|
173
|
+
};
|
|
160
174
|
|
|
161
175
|
const STAGE_ALL = new Set(["-A", "--all", "-u", "--update", "."]);
|
|
176
|
+
const BOOLEAN_SHORT = /[enqsvz]/;
|
|
162
177
|
|
|
163
|
-
/**
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
/** `-a` inside a short-flag cluster such as `-sa`; a value-taking letter ends the cluster (`-mtab`, `-uall`). */
|
|
179
|
+
function clusterHasA(arg: string): boolean {
|
|
180
|
+
if (!/^-[a-zA-Z]+$/.test(arg)) return false;
|
|
181
|
+
for (const ch of arg.slice(1)) {
|
|
182
|
+
if (ch === "a") return true;
|
|
183
|
+
if (!BOOLEAN_SHORT.test(ch)) return false;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The command re-stages every tracked change of this repository before it commits: `commit -a`, or
|
|
190
|
+
* `git add -A|-u|.` with no pathspec, in the same directory, ahead of the commit. Read from parsed
|
|
191
|
+
* invocations, not message text.
|
|
192
|
+
*/
|
|
193
|
+
function stagesAllTracked(invocations: ReturnType<typeof resolveGit>["invocations"]): boolean {
|
|
194
|
+
const firstCommit = invocations.findIndex((i) => i.sub === "commit");
|
|
195
|
+
return invocations.some((i, index) => {
|
|
196
|
+
if (i.dir !== undefined) return false;
|
|
197
|
+
if (i.sub === "commit") return i.args.some((a) => a === "--all" || clusterHasA(a));
|
|
198
|
+
if (i.sub !== "add" || (firstCommit !== -1 && index > firstCommit)) return false;
|
|
199
|
+
const paths = i.args.filter((a) => !a.startsWith("-"));
|
|
200
|
+
return i.args.some((a) => STAGE_ALL.has(a)) && paths.every((a) => a === ".");
|
|
201
|
+
});
|
|
174
202
|
}
|
|
175
203
|
|
|
176
204
|
async function changedNames(
|