Gitfed
bastien-mrq/gitfed/ Commits/ af7c95c

Don't show a repo as empty just because HEAD is dangling

resolveDefaultRef tried HEAD, then fell back to the single branch if there was exactly one — anything else (HEAD dangling with 0 or 2+ branches) reported "not found", which the repo page renders as an empty repo even though real content exists on some other branch. Found live: a repo created with gitfed's InitBareRepo (HEAD -> refs/heads/main) that only ever had "master" pushed to it, plus a second branch — looked completely empty on the repo page despite having real commits, confirmed reachable via `git clone`. Now: main or master wins if either actually exists, regardless of how many branches there are; otherwise the alphabetically-first branch, still better than declaring the repo empty. ok is only ever false when there are truly zero branches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

bastien-mrq 2026-08-03 12:09 commit af7c95c09b0e1d52845cd7e5b52f8b1ecce857d6 parent 4dd7f1fa774d9df8d9ef7b9b7449e816b329b363
2 files changed +96 −5
M internal/gitexec/gitexec.go +17 −5
M internal/gitexec/gitexec_test.go +79 −0
internal/gitexec/gitexec.go
diff --git a/internal/gitexec/gitexec.go b/internal/gitexec/gitexec.go index 3690674..f2ea65e 100644 --- a/internal/gitexec/gitexec.go +++ b/internal/gitexec/gitexec.go @@ -188,10 +188,14 @@ func Serve(verb Verb, path string, stdin io.Reader, stdout, stderr io.Writer) er // resolveDefaultRef returns the tree-ish gitfed treats as this repo's // default branch. It tries HEAD first, but a bare repo's HEAD symref is set // once at creation time (see InitBareRepo) and doesn't move just because a -// client pushes a differently-named branch — so a repo that's had exactly -// one branch pushed to it, ever, is unambiguous even when HEAD is stale. -// ok is false only for a genuinely empty or ambiguous (multiple branches, -// none of them HEAD) repo. +// client pushes a differently-named branch — so HEAD can end up dangling +// (pointing at a branch that was never actually pushed) even for a repo +// that clearly has real content on some other branch. When that happens, +// "main" or "master" wins if either exists (the two names any repo is +// realistically going to use), and otherwise the alphabetically-first +// branch — an arbitrary but deterministic pick beats declaring the repo +// empty just because HEAD points nowhere. ok is false only when there are +// no branches at all. func resolveDefaultRef(repoPath string) (ref string, ok bool) { if exec.Command("git", "--git-dir="+repoPath, "rev-parse", "--verify", "-q", "HEAD").Run() == nil { return "HEAD", true @@ -201,9 +205,17 @@ func resolveDefaultRef(repoPath string) (ref string, ok bool) { return "", false } refs := strings.Fields(string(out)) - if len(refs) != 1 { + if len(refs) == 0 { return "", false } + for _, preferred := range [2]string{"refs/heads/main", "refs/heads/master"} { + for _, r := range refs { + if r == preferred { + return preferred, true + } + } + } + sort.Strings(refs) return refs[0], true }
internal/gitexec/gitexec_test.go
diff --git a/internal/gitexec/gitexec_test.go b/internal/gitexec/gitexec_test.go index 3c9b7c1..d9b539f 100644 --- a/internal/gitexec/gitexec_test.go +++ b/internal/gitexec/gitexec_test.go @@ -143,6 +143,85 @@ func TestReadFileAtHEADEmptyRepo(t *testing.T) { // to "master", or someone names their trunk something else entirely). HEAD // then never resolves even though the repo plainly has content — this is // the exact scenario that made every freshly pushed repo 404 in gitfed-web. +// TestDefaultBranchNameDanglingHeadPrefersMainOrMaster reproduces the real +// case found live: a bare repo whose HEAD points at "main" (InitBareRepo's +// default) but which only ever had "master" pushed to it — with more than +// one branch present so the old single-branch fallback in resolveDefaultRef +// didn't apply and the repo looked empty when browsing it. +func TestDefaultBranchNameDanglingHeadPrefersMainOrMaster(t *testing.T) { + tmp := t.TempDir() + barePath := filepath.Join(tmp, "repo.git") + if err := InitBareRepo(barePath); err != nil { + t.Fatalf("init bare repo: %v", err) + } + + work := filepath.Join(tmp, "work") + if err := os.Mkdir(work, 0755); err != nil { + t.Fatal(err) + } + run(t, work, "git", "init", "-q", "-b", "master") // never "main" + if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0644); err != nil { + t.Fatal(err) + } + run(t, work, "git", "add", "f") + run(t, work, "git", "commit", "-q", "-m", "initial") + run(t, work, "git", "remote", "add", "origin", barePath) + run(t, work, "git", "push", "-q", "origin", "master") + // A second branch, so the old "exactly one branch" fallback doesn't + // mask what's being tested here. + run(t, work, "git", "checkout", "-q", "-b", "some-feature") + run(t, work, "git", "push", "-q", "origin", "some-feature") + + if exec.Command("git", "--git-dir="+barePath, "rev-parse", "--verify", "-q", "HEAD").Run() == nil { + t.Fatal("test setup invalid: HEAD unexpectedly resolves, mismatch not reproduced") + } + + branch, ok := DefaultBranchName(barePath) + if !ok || branch != "master" { + t.Fatalf("DefaultBranchName: got %q ok=%v, want \"master\" (preferred over the arbitrary \"some-feature\")", branch, ok) + } + + root, found, err := ListTree(barePath, "") + if err != nil { + t.Fatalf("ListTree: %v", err) + } + if !found || len(root) != 1 || root[0].Name != "f" { + t.Fatalf("ListTree: found=%v root=%+v, want [f] — the repo must not look empty", found, root) + } +} + +// TestDefaultBranchNameDanglingHeadNoConventionalName covers the remaining +// case: HEAD dangling, several branches, and none named "main" or +// "master" — an arbitrary but deterministic pick beats reporting the repo +// as empty. +func TestDefaultBranchNameDanglingHeadNoConventionalName(t *testing.T) { + tmp := t.TempDir() + barePath := filepath.Join(tmp, "repo.git") + if err := InitBareRepo(barePath); err != nil { + t.Fatalf("init bare repo: %v", err) + } + + work := filepath.Join(tmp, "work") + if err := os.Mkdir(work, 0755); err != nil { + t.Fatal(err) + } + run(t, work, "git", "init", "-q", "-b", "zeta") + if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0644); err != nil { + t.Fatal(err) + } + run(t, work, "git", "add", "f") + run(t, work, "git", "commit", "-q", "-m", "initial") + run(t, work, "git", "remote", "add", "origin", barePath) + run(t, work, "git", "push", "-q", "origin", "zeta") + run(t, work, "git", "checkout", "-q", "-b", "alpha") + run(t, work, "git", "push", "-q", "origin", "alpha") + + branch, ok := DefaultBranchName(barePath) + if !ok || branch != "alpha" { + t.Fatalf("DefaultBranchName: got %q ok=%v, want \"alpha\" (alphabetically first of alpha/zeta)", branch, ok) + } +} + func TestPushToMismatchedDefaultBranch(t *testing.T) { tmp := t.TempDir() barePath := filepath.Join(tmp, "repo.git")