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")