diff --git a/internal/webui/webui.go b/internal/webui/webui.go
index ca8e65c..b00205e 100644
--- a/internal/webui/webui.go
+++ b/internal/webui/webui.go
@@ -115,6 +115,12 @@ func (h *Handler) Register(mux *http.ServeMux) {
mux.HandleFunc("POST /login", h.postLogin)
mux.HandleFunc("POST /logout", h.postLogout)
+ // The bare root is the git smart-HTTP catch-all's territory, but a browser
+ // hitting it should land in the UI rather than a 404. GET /{$} matches only
+ // the exact root, so it never shadows a repo path (/owner/repo/…). Registered
+ // with the UI, so it exists only when the UI does.
+ mux.HandleFunc("GET /{$}", h.redirectRepos)
+
mux.HandleFunc("GET /ui/{$}", h.requireAdmin(h.redirectRepos))
mux.HandleFunc("GET /ui/repos", h.requireAdmin(h.getRepos))
mux.HandleFunc("POST /ui/repos", h.requireAdmin(h.postRepo))
diff --git a/internal/webui/webui_test.go b/internal/webui/webui_test.go
index 5358c95..901a83b 100644
--- a/internal/webui/webui_test.go
+++ b/internal/webui/webui_test.go
@@ -112,6 +112,19 @@ func (x *harness) login(raw string) *http.Cookie {
return nil
}
+// TestRootRedirectsToUI: the bare root sends a browser into the UI instead of
+// 404ing. It matches only the exact root, leaving repo paths to the git handler.
+func TestRootRedirectsToUI(t *testing.T) {
+ x := newHarness(t)
+ rec := x.do(http.MethodGet, "/", nil, nil)
+ if rec.Code != http.StatusSeeOther {
+ t.Fatalf("GET / = %d, want 303", rec.Code)
+ }
+ if loc := rec.Header().Get("Location"); loc != "/ui/repos" {
+ t.Errorf("GET / redirected to %q, want /ui/repos", loc)
+ }
+}
+
func TestGoldenPath(t *testing.T) {
x := newHarness(t)
ctx := context.Background()