@fullstackhouse/cerber 0.27.0 → 0.27.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/core/gh.js +81 -1
- package/package.json +1 -1
package/dist/core/gh.js
CHANGED
|
@@ -90,8 +90,88 @@ async function membershipCheck(args) {
|
|
|
90
90
|
throw err;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* GitHub refuses to render a diff past 300 changed files: `gh pr diff` comes
|
|
95
|
+
* back 406 and the review dies before it starts. The files API answers for ten
|
|
96
|
+
* times as many, so a too-large diff is re-assembled from it rather than
|
|
97
|
+
* reported as a broken review — see {@link assembleDiff}.
|
|
98
|
+
*/
|
|
93
99
|
export async function fetchPrDiff(ref) {
|
|
94
|
-
|
|
100
|
+
try {
|
|
101
|
+
return await gh(["pr", "diff", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`]);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
// Matched on GitHub's own `too_large` signature rather than the bare 406:
|
|
105
|
+
// 406 is "Not Acceptable" generally, while `too_large` is emitted for this
|
|
106
|
+
// refusal and nothing else. Matching the meaning survives a status change;
|
|
107
|
+
// matching the status would fall back on some unrelated 406 one day.
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
if (!/too_large|exceeded the maximum number of files/i.test(message))
|
|
110
|
+
throw err;
|
|
111
|
+
const files = await fetchPrFiles(ref);
|
|
112
|
+
const diff = assembleDiff(files);
|
|
113
|
+
// The files API stops at 3000 too, and says nothing when it does. Hitting
|
|
114
|
+
// the cap exactly is not proof of truncation — a PR can change exactly
|
|
115
|
+
// 3000 files — and there is nothing here to tell the two apart, so the
|
|
116
|
+
// note reports the doubt rather than asserting a loss that may not exist.
|
|
117
|
+
return files.length < FILES_API_CAP
|
|
118
|
+
? diff
|
|
119
|
+
: `${diff}[GitHub's files API returns at most ${FILES_API_CAP} files and returned exactly that many — if this PR changes more, the rest is missing from this diff. Read the checkout.]\n`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const FILES_API_CAP = 3000;
|
|
123
|
+
async function fetchPrFiles(ref) {
|
|
124
|
+
const out = await gh([
|
|
125
|
+
"api",
|
|
126
|
+
"--paginate",
|
|
127
|
+
`repos/${ref.owner}/${ref.repo}/pulls/${ref.number}/files?per_page=100`,
|
|
128
|
+
"--jq",
|
|
129
|
+
".[] | {filename, status, previous_filename, additions, deletions, patch}",
|
|
130
|
+
]);
|
|
131
|
+
// --jq streams one object per line rather than a JSON array.
|
|
132
|
+
return out
|
|
133
|
+
.split("\n")
|
|
134
|
+
.filter((line) => line.trim() !== "")
|
|
135
|
+
.map((line) => JSON.parse(line));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Put the `diff --git` / `---` / `+++` headers back on the per-file hunks the
|
|
139
|
+
* files API returns, so the result parses exactly like `gh pr diff` output —
|
|
140
|
+
* `splitDiffByFile` and the line anchoring read both the same way.
|
|
141
|
+
*
|
|
142
|
+
* A file whose patch GitHub withheld still gets its headers and a line saying
|
|
143
|
+
* so: an unexplained gap would read as "nothing changed here".
|
|
144
|
+
*/
|
|
145
|
+
export function assembleDiff(files) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const renamed = file.status === "renamed" && file.previous_filename;
|
|
149
|
+
const oldPath = file.status === "added" ? null : renamed ? file.previous_filename : file.filename;
|
|
150
|
+
const newPath = file.status === "removed" ? null : file.filename;
|
|
151
|
+
// The side a file does not have is /dev/null, in the headers and in the
|
|
152
|
+
// binary line alike — that is how git names it.
|
|
153
|
+
const a = oldPath ? `a/${oldPath}` : "/dev/null";
|
|
154
|
+
const b = newPath ? `b/${newPath}` : "/dev/null";
|
|
155
|
+
out.push(`diff --git a/${oldPath ?? file.filename} b/${newPath ?? file.filename}`);
|
|
156
|
+
if (renamed)
|
|
157
|
+
out.push(`rename from ${oldPath}`, `rename to ${newPath}`);
|
|
158
|
+
if (file.patch) {
|
|
159
|
+
out.push(`--- ${a}`, `+++ ${b}`, file.patch.replace(/\n$/, ""));
|
|
160
|
+
}
|
|
161
|
+
else if (file.additions > 0 || file.deletions > 0) {
|
|
162
|
+
// GitHub counted the lines and then withheld the patch: a text file it
|
|
163
|
+
// decided was too big. Say so — an empty file block reads as "unchanged".
|
|
164
|
+
out.push(`--- ${a}`, `+++ ${b}`, `[GitHub withheld this file's patch — ${file.additions} addition(s), ${file.deletions} deletion(s). Read the file in the checkout.]`);
|
|
165
|
+
}
|
|
166
|
+
else if (!renamed) {
|
|
167
|
+
out.push(`Binary files ${a} and ${b} differ`);
|
|
168
|
+
}
|
|
169
|
+
// A pure rename ends here, with no hunks and no ---/+++ pair: nothing about
|
|
170
|
+
// the content changed, and `rename from`/`rename to` have already said
|
|
171
|
+
// everything. Calling it binary — which counting lines alone would — is
|
|
172
|
+
// both wrong and the more alarming of the two ways to be wrong.
|
|
173
|
+
}
|
|
174
|
+
return out.length === 0 ? "" : out.join("\n") + "\n";
|
|
95
175
|
}
|
|
96
176
|
/**
|
|
97
177
|
* Map `gh search prs` JSON output to PR refs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "AI code-review cockpit: Claude reviews your PRs into local artifacts; nothing reaches GitHub until you say so.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Full Stack House (https://fullstack.house)",
|