From 57d872fe492fdb9198faaa7868bff9e4ea69dea4 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Fri, 6 Nov 2026 09:46:19 +0000 Subject: [PATCH] fix(github): return 409 for ref lookups in an empty repository GitHub answers GET /repos/{owner}/{repo}/git/ref/{ref} on a repository with no commits with 409 "Git Repository is empty.". The emulator returned 404, which clients cannot tell apart from a missing branch. That forced aictrl to add an emulator-only fallback (aictrl-dev/aictrl#6942). A missing branch in a populated repository still returns 404, and the ref resolves normally after the first commit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Br5cc4QAYrB1gYBPmKLEz5 --- .../src/__tests__/contents-commits.test.ts | 24 +++++++++++++++++++ .../@emulators/github/src/routes/branches.ts | 5 ++++ 2 files changed, 29 insertions(+) diff --git a/packages/@emulators/github/src/__tests__/contents-commits.test.ts b/packages/@emulators/github/src/__tests__/contents-commits.test.ts index e3fd43807..a7f4c93df 100644 --- a/packages/@emulators/github/src/__tests__/contents-commits.test.ts +++ b/packages/@emulators/github/src/__tests__/contents-commits.test.ts @@ -1678,6 +1678,30 @@ describe("GitHub commits routes", () => { expect(res.status).toBe(409); }); + it("409s on a branch ref lookup in an empty repository, like GitHub", async () => { + const res = await app.request(`${base}/repos/octocat/empty-repo/git/ref/heads/main`, { headers: authHeaders() }); + expect(res.status).toBe(409); + expect(((await res.json()) as { message: string }).message).toBe("Git Repository is empty."); + }); + + it("still 404s on a missing branch ref in a populated repository", async () => { + const res = await app.request(`${base}/repos/octocat/hello-world/git/ref/heads/no-such-branch`, { + headers: authHeaders(), + }); + expect(res.status).toBe(404); + }); + + it("serves the branch ref once an empty repository receives its first commit", async () => { + const created = await app.request(`${base}/repos/octocat/empty-repo/contents/README.md`, { + method: "PUT", + headers: jsonHeaders(), + body: JSON.stringify({ message: "first", content: Buffer.from("hi\n").toString("base64") }), + }); + expect(created.status).toBe(201); + const res = await app.request(`${base}/repos/octocat/empty-repo/git/ref/heads/main`, { headers: authHeaders() }); + expect(res.status).toBe(200); + }); + it("returns a single commit with files and stats", async () => { const created = await putFile(app, "notes.txt", "one\ntwo\n"); const createdBody = (await created.json()) as { commit: { sha: string } }; diff --git a/packages/@emulators/github/src/routes/branches.ts b/packages/@emulators/github/src/routes/branches.ts index df3b3a519..7d5ed6da6 100644 --- a/packages/@emulators/github/src/routes/branches.ts +++ b/packages/@emulators/github/src/routes/branches.ts @@ -687,6 +687,11 @@ export function branchesAndGitRoutes({ app, store, webhooks, baseUrl }: RouteCon const repo = lookupRepo(gh, owner, repoName); if (!repo) throw notFoundResponse(); assertRepoContentsRead(gh, c.get("authUser"), repo); + // GitHub answers a ref lookup in a repository with no commits with 409, + // not 404, so clients can tell "empty" from "missing branch". + if (gh.commits.findBy("repo_id", repo.id).length === 0) { + throw new ApiError(409, "Git Repository is empty."); + } const fullRef = fullRefFromParam(refParam); const r = gh.refs.findBy("repo_id", repo.id).find((x) => x.ref === fullRef); if (!r) throw notFoundResponse();