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/web_test.go +16−6
| @@ -128,9 +128,9 @@ func TestCommitPageEscapesFileContent(t *testing.T) { | |||
| 128 | 128 | } | |
| 129 | 129 | } | |
| 130 | 130 | ||
| 131 | // TestReadmeIsTheRepoFrontPage: a forge that shows a bare file list is a | ||
| 132 | // filesystem, not a project page. | ||
| 133 | func TestReadmeIsTheRepoFrontPage(t *testing.T) { | ||
| 131 | // TestReadmeIsTheProjectOverview: the project page has the README, while the | ||
| 132 | // chronological feed has its own tab. | ||
| 133 | func TestReadmeIsTheProjectOverview(t *testing.T) { | ||
| 134 | 134 | t.Parallel() | |
| 135 | 135 | f := newFixture(t) | |
| 136 | 136 | f.publish(f.repo) | |
| @@ -143,16 +143,26 @@ func TestReadmeIsTheRepoFrontPage(t *testing.T) { | |||
| 143 | 143 | } | |
| 144 | 144 | code, body := f.get("/r/" + f.repo) | |
| 145 | 145 | if code != 200 { | |
| 146 | t.Fatalf("repo page = %d", code) | ||
| 146 | t.Fatalf("overview page = %d", code) | ||
| 147 | 147 | } | |
| 148 | 148 | if !strings.Contains(body, "<h1>Demo project</h1>") || !strings.Contains(body, "How to run it.") { | |
| 149 | t.Fatalf("README was not rendered on the repo page:\n%s", body) | ||
| 149 | t.Fatalf("README was not rendered on the overview page:\n%s", body) | ||
| 150 | } | ||
| 151 | // The feed has its own tab, and the code tab still renders the README too. | ||
| 152 | if code, feed := f.get("/r/" + f.repo + "/activity"); code != 200 || !strings.Contains(feed, "Activity") { | ||
| 153 | t.Fatalf("repo activity page = %d", code) | ||
| 154 | } | ||
| 155 | if code, codePage := f.get("/r/" + f.repo + "/code"); code != 200 || !strings.Contains(codePage, "Demo project") { | ||
| 156 | t.Fatalf("code page = %d", code) | ||
| 150 | 157 | } | |
| 151 | 158 | // Markdown blobs render too, while source files stay source. | |
| 152 | 159 | if code, body := f.get("/r/" + f.repo + "/blob/main/README.md"); code != 200 || !strings.Contains(body, "<h1>Demo project</h1>") { | |
| 153 | 160 | t.Fatalf("markdown blob = %d:\n%s", code, body) | |
| 154 | 161 | } | |
| 155 | if code, body := f.get("/r/" + f.repo + "/blob/main/main.go"); code != 200 || !strings.Contains(body, "package main") { | ||
| 162 | // Source is highlighted, so "package main" arrives as token spans rather than | ||
| 163 | // one contiguous string; what matters is that it is code, not rendered markdown. | ||
| 164 | if code, body := f.get("/r/" + f.repo + "/blob/main/main.go"); code != 200 || | ||
| 165 | !strings.Contains(body, `class="chroma"`) || !strings.Contains(body, `>package</span>`) { | ||
| 156 | 166 | t.Fatalf("source blob = %d", code) | |
| 157 | 167 | } | |
| 158 | 168 | } | |