Imports say why the server refused a push (#133)
A failed import push reported git's generic last line ("failed to push some
refs"), which hid the server's reason. The importer now reports the reason on
the rejected ref (from --porcelain output), or the last specific stderr line.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AizKXSsengXZRegJxRATkLCanberk Demirci authored · GitHub committed signed1 parent 92fbc27commit 3a392011adce9d8769e71838d99126b46ec3d876Browse files
2 files changed+19−4
modifiedcontainers/runner/src/importer.ts+13−3View file
| @@ -218,7 +218,7 @@ | ||
| 218 | 218 | const status = new Map(parsePorcelain(out.stdout).map((s) => [s.ref, s])); |
| 219 | 219 | for (const r of batch) { |
| 220 | 220 | const s = status.get(importDestination(r)); |
| 221 | - if (out.code !== 0 && !s) throw new ImportError(`pushing refs failed: ${lastLine(out.stderr)}`); | |
| 221 | + if (out.code !== 0 && !s) throw new ImportError(`pushing refs failed: ${pushFailure(out)}`); | |
| 222 | 222 | if (s && !s.ok) { |
| 223 | 223 | result.warnings.push(`${r} was not imported: ${s.reason}`); |
| 224 | 224 | continue; |
| @@ -262,6 +262,16 @@ | ||
| 262 | 262 | .slice(-1)[0] |
| 263 | 263 | ?.slice(0, 300) ?? ""; |
| 264 | 264 | |
| 265 | +/** | |
| 266 | + * Why a push failed, in the server's words: the reason on a rejected ref (`--porcelain` puts it on | |
| 267 | + * stdout), else the last line of stderr other than git's generic "failed to push some refs". | |
| 268 | + */ | |
| 269 | +export const pushFailure = (out: { stdout: string; stderr: string }) => | |
| 270 | + parsePorcelain(out.stdout).find((s) => !s.ok)?.reason.slice(0, 300) || | |
| 271 | + lastLine(out.stderr.split("\n").filter((l) => !/^error: failed to push some refs/.test(l)).join("\n")) || | |
| 272 | + lastLine(out.stderr) || | |
| 273 | + lastLine(out.stdout); | |
| 274 | + | |
| 265 | 275 | /** Count and size of the LFS objects in a bare repository's local store. */ |
| 266 | 276 | async function lfsStore(gitDir: string): Promise<{ count: number; bytes: number }> { |
| 267 | 277 | let count = 0; |
| @@ -429,13 +439,13 @@ | ||
| 429 | 439 | } |
| 430 | 440 | // Chunking can no longer help. Say what the server answered: the object may be under the |
| 431 | 441 | // 100 MB push limit and still be refused (or the server may be failing for another reason). |
| 432 | - const said = lastLine(out.stderr) || lastLine(out.stdout); | |
| 442 | + const said = pushFailure(out); | |
| 433 | 443 | if (size > this.budget) { |
| 434 | 444 | throw new ImportError(`a single Git object of ${fmtBytes(size)} could not be pushed even on its own (${said}). Remove the large file from the repository's history (e.g. with git filter-repo) and import again.`); |
| 435 | 445 | } |
| 436 | 446 | throw new ImportError(`pushes kept failing even in chunks of ${fmtBytes(this.budget)} (${said})`); |
| 437 | 447 | } |
| 438 | - throw new ImportError(`push failed: ${lastLine(out.stderr) || lastLine(out.stdout)}`); | |
| 448 | + throw new ImportError(`push failed: ${pushFailure(out)}`); | |
| 439 | 449 | } |
| 440 | 450 | |
| 441 | 451 | /** `git push` to Diffinery in import mode; collects push protection findings from the output. */ |
modifiedcontainers/runner/test/import.test.ts+6−1View file
| @@ -6,7 +6,7 @@ | ||
| 6 | 6 | import { describe, expect, it } from "vitest"; |
| 7 | 7 | import type { ImportSpec } from "@diffinery/shared"; |
| 8 | 8 | import { batchBySize, furthestWithin, importDestination, importRefKind, isSecretRejection, isSizeRejection, parsePorcelain, parseSecretFindings, sortRefsForImport } from "../src/import-plan.js"; |
| 9 | -import { transfer } from "../src/importer.js"; | |
| 9 | +import { pushFailure, transfer } from "../src/importer.js"; | |
| 10 | 10 | |
| 11 | 11 | describe("import chunk planning", () => { |
| 12 | 12 | it("finds the furthest commit within budget with few probes", async () => { |
| @@ -53,6 +53,11 @@ | ||
| 53 | 53 | { ref: "refs/heads/a", ok: true, reason: "[new branch]" }, |
| 54 | 54 | { ref: "refs/heads/b", ok: false, reason: "invalid ref name" }, |
| 55 | 55 | ]); |
| 56 | + // A failed push says why in the server's words, not git's generic last line. | |
| 57 | + const generic = "error: failed to push some refs to 'https://diffinery.com/o/r.git'"; | |
| 58 | + expect(pushFailure({ stdout: "To x\n!\trefs/heads/b:refs/heads/b\t[remote rejected] (missing objects)\nDone\n", stderr: generic })).toBe("missing objects"); | |
| 59 | + expect(pushFailure({ stdout: "", stderr: `error: remote unpack failed: unpacker error\n${generic}` })).toBe("error: remote unpack failed: unpacker error"); | |
| 60 | + expect(pushFailure({ stdout: "", stderr: generic })).toBe(generic); | |
| 56 | 61 | }); |
| 57 | 62 | }); |
| 58 | 63 |