Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

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_01AizKXSsengXZRegJxRATkL
Canberk 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 @@
218218 const status = new Map(parsePorcelain(out.stdout).map((s) => [s.ref, s]));
219219 for (const r of batch) {
220220 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)}`);
222222 if (s && !s.ok) {
223223 result.warnings.push(`${r} was not imported: ${s.reason}`);
224224 continue;
@@ -262,6 +262,16 @@
262262 .slice(-1)[0]
263263 ?.slice(0, 300) ?? "";
264264
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+
265275 /** Count and size of the LFS objects in a bare repository's local store. */
266276 async function lfsStore(gitDir: string): Promise<{ count: number; bytes: number }> {
267277 let count = 0;
@@ -429,13 +439,13 @@
429439 }
430440 // Chunking can no longer help. Say what the server answered: the object may be under the
431441 // 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);
433443 if (size > this.budget) {
434444 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.`);
435445 }
436446 throw new ImportError(`pushes kept failing even in chunks of ${fmtBytes(this.budget)} (${said})`);
437447 }
438- throw new ImportError(`push failed: ${lastLine(out.stderr) || lastLine(out.stdout)}`);
448+ throw new ImportError(`push failed: ${pushFailure(out)}`);
439449 }
440450
441451 /** `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 @@
66 import { describe, expect, it } from "vitest";
77 import type { ImportSpec } from "@diffinery/shared";
88 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";
1010
1111 describe("import chunk planning", () => {
1212 it("finds the furthest commit within budget with few probes", async () => {
@@ -53,6 +53,11 @@
5353 { ref: "refs/heads/a", ok: true, reason: "[new branch]" },
5454 { ref: "refs/heads/b", ok: false, reason: "invalid ref name" },
5555 ]);
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);
5661 });
5762 });
5863