diff --git a/internal/webui/browse.go b/internal/webui/browse.go
index a875853..98f4bda 100644
--- a/internal/webui/browse.go
+++ b/internal/webui/browse.go
@@ -130,16 +130,26 @@ func (h *Handler) browseCommitRoute(w http.ResponseWriter, r *http.Request) {
h.renderCommit(w, r, c, r.PathValue("sha"))
}
-// browseRefsRoute serves /{repo}/refs: the repo's branches and tags.
+// browseRefsRoute serves /{repo}/refs: the repo's branches and tags. It resolves
+// no ref of its own — deliberately, since it is the redirect target when the
+// default branch is missing, so it must render even then (and it reads refs from
+// meta, needing no materialization). Calling the redirecting resolve here would
+// loop that redirect back onto itself.
func (h *Handler) browseRefsRoute(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("repo")
if !h.authorizeRead(w, r, repoName) {
return
}
- c, _, ok := h.resolve(w, r, repoName, "")
- if !ok {
+ rp, err := h.md.GetRepo(r.Context(), repoName)
+ if errors.Is(err, meta.ErrNotFound) {
+ http.NotFound(w, r)
return
}
+ if err != nil {
+ h.serverError(w, "get repo", err)
+ return
+ }
+ c := browseCtx{repo: rp, ref: rp.DefaultBranch}
h.render(w, "browse_refs.html", refsData{
browseBase: h.browseHeader(r, c),
Refs: h.refChoices(r, c.repo.ID),
@@ -230,7 +240,9 @@ type browseCtx struct {
// against the repo's refs — the longest leading prefix of rest that names a real
// branch or tag (branch beats a tag on a tie); an empty rest selects the default
// branch. It writes a 404 for an unknown repo, a rest that names no ref, or a
-// ref that fails to resolve (and a 500 for anything else), returning ok=false.
+// ref that fails to resolve (and a 500 for anything else), returning ok=false —
+// except a missing *default* branch (empty rest), which redirects to the repo's
+// ref list rather than dead-ending on a 404.
func (h *Handler) resolve(w http.ResponseWriter, r *http.Request, repoName, rest string) (browseCtx, string, bool) {
ctx := r.Context()
rp, err := h.md.GetRepo(ctx, repoName)
@@ -261,6 +273,14 @@ func (h *Handler) resolve(w http.ResponseWriter, r *http.Request, repoName, rest
// bare-name precedence favors tags) and an annotated tag peels to its commit.
sha, err := repo.ResolveRef(ctx, dir, qualify(ref, branch))
if errors.Is(err, repo.ErrNotFound) {
+ if strings.Trim(rest, "/") == "" {
+ // Empty rest selected the configured default branch, but it doesn't
+ // exist (e.g. default "main" while the repo pushed "master") — there is
+ // no landing content, so send the visitor to the ref list to pick a real
+ // branch instead of a dead-end 404.
+ http.Redirect(w, r, "/"+repoName+"/refs", http.StatusFound)
+ return browseCtx{}, "", false
+ }
http.NotFound(w, r)
return browseCtx{}, "", false
}
diff --git a/internal/webui/browse_test.go b/internal/webui/browse_test.go
index 9ec1c8c..3476764 100644
--- a/internal/webui/browse_test.go
+++ b/internal/webui/browse_test.go
@@ -450,3 +450,38 @@ func TestBrowseNotFoundAndTraversal(t *testing.T) {
})
}
}
+
+// TestBrowseLandingMissingDefaultBranchRedirectsToRefs: when a repo's configured
+// default branch doesn't exist (e.g. default "main" but the repo pushed
+// "master"), the bare /{repo} landing redirects to the ref list instead of 404ing.
+func TestBrowseLandingMissingDefaultBranchRedirectsToRefs(t *testing.T) {
+ x := newHarness(t)
+ x.seedBrowseRepo("app", "master") // real branch: master
+ r, err := x.md.GetRepo(context.Background(), "app")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Point the default branch at a branch that doesn't exist.
+ if err := x.md.SetDefaultBranch(context.Background(), r.ID, "main"); err != nil {
+ t.Fatalf("SetDefaultBranch: %v", err)
+ }
+ session := x.login(x.mintTokenFor(x.admin.ID))
+
+ rec := x.do(http.MethodGet, "/app", nil, session)
+ if rec.Code != http.StatusFound {
+ t.Fatalf("landing = %d, want 302 redirect (%s)", rec.Code, rec.Body)
+ }
+ if loc := rec.Header().Get("Location"); loc != "/app/refs" {
+ t.Errorf("redirect Location = %q, want /app/refs", loc)
+ }
+
+ // The redirect target must render, not redirect again (else ERR_TOO_MANY_REDIRECTS):
+ // the ref list stands on its own even with the default branch missing.
+ refs := x.do(http.MethodGet, "/app/refs", nil, session)
+ if refs.Code != http.StatusOK {
+ t.Fatalf("refs page = %d, want 200 (a redirect here is the loop bug)", refs.Code)
+ }
+ if !strings.Contains(refs.Body.String(), "master") {
+ t.Errorf("refs page should list the real branch (master): %s", refs.Body)
+ }
+}
diff --git a/internal/webui/templates/dashboard.html b/internal/webui/templates/dashboard.html
index 491f4ba..e7de225 100644
--- a/internal/webui/templates/dashboard.html
+++ b/internal/webui/templates/dashboard.html
@@ -3,11 +3,23 @@
{{if .Repos}}
<table>
- <thead><tr><th>Name</th><th>Visibility</th><th>Default branch</th>{{if .IsAdmin}}<th>Manage</th>{{end}}</tr></thead>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Visibility</th>
+ <th>Default branch</th>
+ {{if .IsAdmin}}
+ <th>Manage</th>
+ {{end}}
+ </tr>
+ </thead>
<tbody>
- {{range .Repos}}
+ {{range .Repos}}
<tr>
- <td><a href="/{{.Name}}">{{.Name}}</a> · <a href="/{{.Name}}/runs">CI</a></td>
+ <td>
+ <a href="/{{.Name}}">{{.Name}}</a>
+ · <a href="/{{.Name}}/refs">refs</a> · <a href="/{{.Name}}/runs">CI</a>
+ </td>
<td>{{.Visibility}}</td>
<td>{{.DefaultBranch}}</td>
{{if $.IsAdmin}}
@@ -18,21 +30,25 @@
</td>
{{end}}
</tr>
- {{end}}
+ {{end}}
</tbody>
</table>
{{else}}
-<p>No repositories to show.{{if not .Me}} <a href="/login">Sign in</a> to see private repositories.{{end}}</p>
-{{end}}
-
-{{if .IsAdmin}}
+<p>
+ No repositories to show.{{if not .Me}} <a href="/login">Sign in</a> to see
+ private repositories.{{end}}
+</p>
+{{end}} {{if .IsAdmin}}
<fieldset>
<legend>Create repository</legend>
<form method="post" action="/">
- <label>Name <input type="text" name="name" placeholder="gitmote" required></label>
- <label>Default branch <input type="text" name="default_branch" value="main"></label>
+ <label
+ >Name <input type="text" name="name" placeholder="gitmote" required
+ /></label>
+ <label
+ >Default branch <input type="text" name="default_branch" value="main"
+ /></label>
<button type="submit">Create</button>
</form>
</fieldset>
-{{end}}
-{{template "bottom" .}}{{end}}
+{{end}} {{template "bottom" .}}{{end}}