vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Build out the forge web UI
f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264
Build out the forge web UI Turn the web surface into a compact, server-rendered forge with HTMX as the progressive dynamic layer. - Split templates and assets into embedded files; add a tokenised dark theme, Lucide icons, and vendored HTMX. - Add the repository Overview page and a separate chronological Activity feed with type filters and HTMX fragment updates. - Finish the issue and change workflows: create pages, comments, close/reopen, labels and assignees, approval, discussion, line-anchored review comments, and HTMX list filters. - Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware code browsing, and a repository Runs list. - Add a loopback-only development login for local write testing, with explicit configuration and listener checks. - Extend tests for the new pages, fragments, labels, assignees, anchors, and development login.
server/server.go +20−1
| @@ -97,6 +97,9 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 97 | 97 | wp.Locks = inst.Locks | |
| 98 | 98 | wp.Sessions = inst.Sessions | |
| 99 | 99 | wp.Logger = func(format string, args ...any) { log.Warn(fmt.Sprintf(format, args...)) } | |
| 100 | if cfg.DevLogin != "" { | ||
| 101 | log.Warn("development login enabled", "account", cfg.DevLogin, "http_addr", cfg.HTTPAddr) | ||
| 102 | } | ||
| 100 | 103 | if wp.Auth != nil && !wp.Auth.Enabled() { | |
| 101 | 104 | log.Info("browser login disabled", "reason", wp.Auth.Disabled) | |
| 102 | 105 | } else if cfg.OpenRegistration { | |
| @@ -139,8 +142,10 @@ func (s *Server) routes() *http.ServeMux { | |||
| 139 | 142 | mux := http.NewServeMux() | |
| 140 | 143 | w := s.Web | |
| 141 | 144 | ||
| 145 | mux.Handle("GET /static/", web.Static()) | ||
| 142 | 146 | mux.HandleFunc("GET /{$}", w.IndexPage) | |
| 143 | 147 | mux.HandleFunc("GET /login", w.LoginPage) | |
| 148 | mux.HandleFunc("GET /dev/login", w.DevLogin) | ||
| 144 | 149 | mux.HandleFunc("GET /register", w.RegisterPage) | |
| 145 | 150 | mux.HandleFunc("POST /auth/register/begin", w.RegisterBegin) | |
| 146 | 151 | mux.HandleFunc("POST /auth/register/finish", w.RegisterFinish) | |
| @@ -158,20 +163,29 @@ func (s *Server) routes() *http.ServeMux { | |||
| 158 | 163 | mux.HandleFunc("POST /account/passkeys/finish", w.AddPasskeyFinish) | |
| 159 | 164 | mux.HandleFunc("GET /auth.js", w.AuthJS) | |
| 160 | 165 | mux.HandleFunc("GET /runners", w.RunnersPage) | |
| 161 | mux.HandleFunc("GET /r/{name}", w.Repo) | ||
| 166 | mux.HandleFunc("GET /r/{name}", w.Overview) | ||
| 167 | mux.HandleFunc("GET /r/{name}/activity", w.Activity) | ||
| 168 | mux.HandleFunc("GET /r/{name}/code", w.Code) | ||
| 162 | 169 | mux.HandleFunc("GET /r/{name}/tree/{ref}/{path...}", w.Tree) | |
| 163 | 170 | mux.HandleFunc("GET /r/{name}/blob/{ref}/{path...}", w.Blob) | |
| 164 | 171 | mux.HandleFunc("GET /r/{name}/raw/{ref}/{path...}", w.Raw) | |
| 165 | 172 | mux.HandleFunc("GET /r/{name}/commits/{ref}", w.Commits) | |
| 166 | 173 | mux.HandleFunc("GET /r/{name}/commit/{sha}", w.CommitView) | |
| 167 | 174 | mux.HandleFunc("GET /r/{name}/issues", w.Issues) | |
| 175 | mux.HandleFunc("GET /r/{name}/issues/new", w.NewIssuePage) | ||
| 168 | 176 | mux.HandleFunc("POST /r/{name}/issues", w.CreateIssue) | |
| 169 | 177 | mux.HandleFunc("GET /r/{name}/issues/{id}", w.IssueView) | |
| 170 | 178 | mux.HandleFunc("POST /r/{name}/issues/{id}/comments", w.AddIssueComment) | |
| 179 | mux.HandleFunc("POST /r/{name}/issues/{id}/state", w.ChangeIssueState) | ||
| 180 | mux.HandleFunc("POST /r/{name}/issues/{id}/labels", w.SetIssueLabels) | ||
| 181 | mux.HandleFunc("POST /r/{name}/issues/{id}/assignees", w.SetIssueAssignees) | ||
| 171 | 182 | mux.HandleFunc("GET /r/{name}/requests", w.Requests) | |
| 183 | mux.HandleFunc("GET /r/{name}/requests/new", w.NewChangePage) | ||
| 172 | 184 | mux.HandleFunc("POST /r/{name}/requests", w.CreateChangeRequest) | |
| 173 | 185 | mux.HandleFunc("GET /r/{name}/requests/{id}", w.RequestView) | |
| 174 | 186 | mux.HandleFunc("POST /r/{name}/requests/{id}/approve", w.ApproveRequest) | |
| 187 | mux.HandleFunc("POST /r/{name}/requests/{id}/comments", w.AddRequestComment) | ||
| 188 | mux.HandleFunc("POST /r/{name}/requests/{id}/state", w.ChangeRequestState) | ||
| 175 | 189 | mux.HandleFunc("POST /r/{name}/ci/allow", w.AllowContributorCI) | |
| 176 | 190 | // A tag may contain "/", so the detail route takes the rest of the path; the | |
| 177 | 191 | // two archive routes are more specific and win for their suffixes. | |
| @@ -181,10 +195,15 @@ func (s *Server) routes() *http.ServeMux { | |||
| 181 | 195 | mux.HandleFunc("GET /r/{name}/releases/{tag}/archive.tar", w.ReleaseTar) | |
| 182 | 196 | mux.HandleFunc("GET /r/{name}/releases/{tag}/archive.tar.gz", w.ReleaseTarGz) | |
| 183 | 197 | mux.HandleFunc("GET /r/{name}/releases/{tag}/artifacts/{artifact}", w.ReleaseArtifact) | |
| 198 | mux.HandleFunc("GET /r/{name}/runs", w.RunsPage) | ||
| 184 | 199 | mux.HandleFunc("GET /r/{name}/runs/{id}", w.RunPage) | |
| 185 | 200 | mux.HandleFunc("GET /r/{name}/runs/{id}/log", w.RunLog) | |
| 186 | 201 | mux.HandleFunc("GET /r/{name}/runs/{id}/artifacts/{artifact}", w.RunArtifact) | |
| 187 | 202 | ||
| 203 | // Unknown paths get the same styled 404 as a missing or private repository. | ||
| 204 | // More specific patterns above still win; this only catches the rest. | ||
| 205 | mux.HandleFunc("/{path...}", w.NotFoundPage) | ||
| 206 | |||
| 188 | 207 | mux.HandleFunc("GET /healthz", func(rw http.ResponseWriter, r *http.Request) { | |
| 189 | 208 | rw.Header().Set("Content-Type", "text/plain") | |
| 190 | 209 | fmt.Fprintf(rw, "ok\nusers=%d\n", s.Identity.Snapshot().Count) | |