Repository navigation
fix(github): return 409 for ref lookups in an empty repository - #3
Conversation
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Br5cc4QAYrB1gYBPmKLEz5
| 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) { |
There was a problem hiding this comment.
🟡 409 gate counts commits; GitHub empties have no refs.
🤖 Fix with your agent
Fix this code review finding (aictrl-dev/github-emulator PR #3, packages/@emulators/github/src/routes/branches.ts:692-694):
Problem: 409 gate counts commits; GitHub empties have no refs
Detail: The new gate treats a repo as empty when it has zero commits, but GitHub's "Git Repository is empty." 409 fires for a repository without branches (no refs). The emulator allows creating dangling Git objects without any ref: existing tests (e.g. "keeps the branch head before newer-dated parents") POST /git/commits and only afterwards PATCH /git/refs — so the store can hold commits with zero refs. In that state this endpoint now falls through to the ref lookup and returns 404, while real GitHub would still answer 409 "Git Repository is empty." — the exact case the PR was written to match. Gating on the refs table is also the self-consistent predicate for a ref endpoint. Repro: Given octocat/empty-repo has no refs; When the client seeds dangling objects via POST /git/blobs, POST /git/trees, POST /git/commits (all 201, no ref created); And calls GET /repos/octocat/empty-repo/git/ref/heads/main; Then the emulator responds 404 but real GitHub responds 409 "Git Repository is empty.".
Suggested fix: Gate on refs instead of commits: `if (gh.refs.findBy("repo_id", repo.id).length === 0) throw new ApiError(409, "Git Repository is empty.");` — matching GitHub's "no branches" definition of an empty repository. If adopted, apply the same predicate to the commits-list handler (commits.ts:99) via the shared helper so the two endpoints stay consistent.
Implement the fix on the PR head branch and add a regression test that fails before the fix and passes after.
Why this matters
The new gate treats a repo as empty when it has zero commits, but GitHub's "Git Repository is empty." 409 fires for a repository without branches (no refs). The emulator allows creating dangling Git objects without any ref: existing tests (e.g. "keeps the branch head before newer-dated parents") POST /git/commits and only afterwards PATCH /git/refs — so the store can hold commits with zero refs. In that state this endpoint now falls through to the ref lookup and returns 404, while real GitHub would still answer 409 "Git Repository is empty." — the exact case the PR was written to match. Gating on the refs table is also the self-consistent predicate for a ref endpoint. Repro: Given octocat/empty-repo has no refs; When the client seeds dangling objects via POST /git/blobs, POST /git/trees, POST /git/commits (all 201, no ref created); And calls GET /repos/octocat/empty-repo/git/ref/heads/main; Then the emulator responds 404 but real GitHub responds 409 "Git Repository is empty.".
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();
return c.json(formatRefJson(gh, repo, r.ref, r.sha, baseUrl));| 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) { |
There was a problem hiding this comment.
🟡 Empty-repo 409 guard duplicated from commits.ts.
🤖 Fix with your agent
Fix this code review finding (aictrl-dev/github-emulator PR #3, packages/@emulators/github/src/routes/branches.ts:692-694):
Problem: Empty-repo 409 guard duplicated from commits.ts
Detail: The new inline guard (branches.ts:692-694) is a verbatim copy of the existing block in the commits-list handler GET /repos/:owner/:repo/commits (packages/@emulators/github/src/routes/commits.ts:99-101): same predicate, same status code, same literal "Git Repository is empty." string. Two hand-maintained copies of the same rule across route files will drift (message wording or the emptiness definition), and the sibling matching-refs handler will need a third copy. The codebase already centralises route guards (notFoundResponse, assertRepoContentsRead) in route-helpers.ts, so this predicate belongs there too.
Suggested fix: Add e.g. `export function assertRepoNotEmpty(gh: GitHubStore, repo: GitHubRepo): void { if (gh.commits.findBy("repo_id", repo.id).length === 0) throw new ApiError(409, "Git Repository is empty."); }` to packages/@emulators/github/src/route-helpers.ts and call it from both the /commits handler (commits.ts:99) and the /git/ref handler (branches.ts:692), deleting the duplicated inline copies (and reuse it in matching-refs when that gap is fixed).
Implement the fix on the PR head branch and add a regression test that fails before the fix and passes after.
Why this matters
The new inline guard (branches.ts:692-694) is a verbatim copy of the existing block in the commits-list handler GET /repos/:owner/:repo/commits (packages/@emulators/github/src/routes/commits.ts:99-101): same predicate, same status code, same literal "Git Repository is empty." string. Two hand-maintained copies of the same rule across route files will drift (message wording or the emptiness definition), and the sibling matching-refs handler will need a third copy. The codebase already centralises route guards (notFoundResponse, assertRepoContentsRead) in route-helpers.ts, so this predicate belongs there too.
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();
return c.json(formatRefJson(gh, repo, r.ref, r.sha, baseUrl));
Code reviewVerdict: Address the major findings before merging. · 🔴 0 · 🟠 1 · 🟡 2 · ⚪ 0 · 0/3 resolved
🤖 Fix all 3 open findings with your agent📋 Out-of-diff findings (3)
Reviewed 2 files · 0 inline · view all 3 findings ↗ · AI-generated — verify findings before acting · AI guidance and feedback aictrl · AI code review for fast-moving teams · aictrl.dev |
Why
When a repository has no commits, GitHub answers
GET /repos/{owner}/{repo}/git/ref/{ref}with 409 "Git Repository is empty." The emulator returned 404, which clients can't tell apart from a missing branch.aictrl relies on that 409 to recognise an empty repository; production logs show it as
github-content 409 "Git Repository is empty.". With the emulator returning 404, the aictrl fix for aictrl-dev/aictrl#6942 needed an emulator-only fallback in production code. Fixing the emulator removes that fallback.Change
GET /repos/{owner}/{repo}/git/ref/{ref}returns 409Git Repository is empty.when the repository has no commits. This matches the existing/commitsbehaviour.Tests
contents-commits.test.ts. The empty-repo case was shown failing first (expected 404 to be 409).pnpm build,type-check,lint,format:checkandtestall pass on Node 24.refs aictrl-dev/aictrl#6942
🤖 Generated with Claude Code
https://claude.ai/code/session_01Br5cc4QAYrB1gYBPmKLEz5