| 2157 | // `cache` (optional) memoizes the result for the run: with several batches each |
| 2158 | // failing 422, the inventory would otherwise be refetched per batch. |
| 2159 | async function getPrDiffHunks({ github, owner, repo, prNumber, commitSha, log, cache }) { |
| 2160 | const logFn = typeof log === "function" ? log : () => {}; |
| 2161 | if (cache && cache.diff !== undefined) return cache.diff; |
| 2162 | |
| 2163 | const files = new Map(); |
| 2164 | const known = new Set(); |
| 2165 | let complete = true; |
| 2166 | |
| 2167 | const PER_PAGE = 100; |
| 2168 | const MAX_PAGES = 30; |
| 2169 | let page = 1; |
| 2170 | while (page <= MAX_PAGES) { |
| 2171 | const res = await readWithPacing( |
| 2172 | `listFiles (page ${page})`, |
| 2173 | () => github.rest.pulls.listFiles({ owner, repo, pull_number: prNumber, per_page: PER_PAGE, page }), |
| 2174 | logFn |
| 2175 | ); |
| 2176 | const batch = (res && res.data) || []; |
| 2177 | for (const file of batch) { |
| 2178 | if (!file || !file.filename) continue; |
| 2179 | known.add(file.filename); |
| 2180 | if (file.patch) { |
| 2181 | const parsed = parseDiffHunkInventory(file.patch); |
| 2182 | if (parsed.complete) { |
| 2183 | files.set(file.filename, parsed.ranges); |
| 2184 | } else { |
| 2185 | logFn( |
| 2186 | `[422-fallback] Patch data for ${file.filename} is incomplete or malformed; ` + |
| 2187 | `comments on that file will be treated as unknown rather than out-of-diff.` |
| 2188 | ); |
| 2189 | } |
| 2190 | } |
| 2191 | } |
| 2192 | if (batch.length < PER_PAGE) break; |
| 2193 | page++; |
| 2194 | } |
| 2195 | if (page > MAX_PAGES) { |
| 2196 | complete = false; |
| 2197 | logFn( |
| 2198 | `[422-fallback] PR changed-file list exceeded ${MAX_PAGES * PER_PAGE} files; ` + |
| 2199 | `diff inventory is incomplete, so no comment will be discarded as out-of-diff.` |
| 2200 | ); |
| 2201 | } |
| 2202 | |
| 2203 | // An empty inventory proves nothing. A PR that produced review comments |
| 2204 | // necessarily has changed files, so an empty listFiles response is an anomaly |
| 2205 | // (diff not yet materialized server-side, or a malformed/empty response body) |
| 2206 | // rather than evidence that every commented path sits outside the diff. |
| 2207 | // Trusting it would classify EVERY comment "invalid" and discard the whole |
| 2208 | // batch without a single posting attempt — the exact outcome the tri-state |
| 2209 | // classification exists to prevent. Note this is the mirror of the truncation |
| 2210 | // case above: too many files and zero files are both "cannot judge". |
| 2211 | if (known.size === 0) { |
| 2212 | complete = false; |
| 2213 | logFn( |
| 2214 | `[422-fallback] PR changed-file list came back empty, which cannot be right for a PR ` + |
| 2215 | `under review; treating the diff inventory as incomplete, so no comment will be ` + |
| 2216 | `discarded as out-of-diff.` |