Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Build a landing page and simplify the forge chrome

074ae1605e177e99533af02d8c69ccd1a6a947bb · Keenan Nemetz · 14d ago · parent 62576fa8

Build a landing page and simplify the forge chrome

The instance root is now a landing page rather than the full activity feed:
forge identity, project cards with top language, last commit, issue/change
counts and latest release, a short activity rail, and a first-run empty state.
The full feed moves to /activity with type filters and pagination.

Feeds now cover issues, changes, comments, and releases only; commits and CI
runs already have their own pages. Configuration gains instance_name and
instance_tagline.

Navigation: the crowded global bar becomes Runners, Activity, search, and an
account menu holding Admin, Status, and Log out. Repository pages show the name
and description above the tabs at the home-page header scale, while tab-page
titles stay small. Top-level pages keep the larger header. Drop the workflow
run chip, assigned-issues strip, repository size stat, and footer Status link.

server/ui_test.go +20−16

@@ -58,15 +58,17 @@ func TestRepoTabShowsNameAndVisibility(t *testing.T) {
58 58 _, cookie := f.browserSession(t, f.ownerID)
59 59 resp, body := f.browserGet(t, "/r/"+f.repo, cookie)
60 60 if resp.StatusCode != 200 || !strings.Contains(body, `class="repotab active"`) ||
61 !strings.Contains(body, `title="Private repository"`) || !strings.Contains(body, ">demo</a>") {
62 t.Fatalf("private repo tab = %d:\n%s", resp.StatusCode, body)
61 !strings.Contains(body, `title="Private repository"`) || !strings.Contains(body, "demo</div>") {
62 t.Fatalf("private repo header = %d:\n%s", resp.StatusCode, body)
63 }
64 if strings.Index(body, `class="repo-name"`) > strings.Index(body, `class="tabs"`) {
65 t.Fatalf("repository title is not above the tabs:\n%s", body)
63 66 }
64 67
65 68 f.publish(f.repo)
66 69 _, body = f.get("/r/" + f.repo + "/code")
67 if !strings.Contains(body, `title="Public repository"`) ||
68 !strings.Contains(body, `aria-label="demo (public repository)"`) {
69 t.Fatalf("public repo tab does not show the globe state:\n%s", body)
70 if !strings.Contains(body, `title="Public repository"`) || !strings.Contains(body, ">Overview</a>") {
71 t.Fatalf("public repo header does not show the globe state or Overview tab:\n%s", body)
70 72 }
71 73 }
72 74
@@ -121,16 +123,12 @@ func TestOverviewAndActivityPages(t *testing.T) {
121 123 t.Fatalf("overview page does not show %q:\n%s", want, body)
122 124 }
123 125 }
124 // The activity tab is a separate page, and its commit filter is a fragment.
126 // The activity tab is a separate page. Commits are not feed events: they
127 // belong to the overview and the code browser.
125 128 code, body = f.get("/r/" + f.repo + "/activity")
126 if code != 200 || !strings.Contains(body, "Activity") || !strings.Contains(body, "initial") {
129 if code != 200 || !strings.Contains(body, "Activity") || strings.Contains(body, "committed") {
127 130 t.Fatalf("activity page = %d:\n%s", code, body)
128 131 }
129 resp, fragment := f.htmxGet(t, "/r/"+f.repo+"/activity?kind=commits", "")
130 if resp.StatusCode != 200 || !strings.Contains(fragment, `id="activity-feed"`) ||
131 strings.Contains(fragment, "<!doctype html>") || !strings.Contains(fragment, "initial") {
132 t.Fatalf("commit activity fragment = %d:\n%s", resp.StatusCode, fragment)
133 }
134 132 // Code browsing moved to its own tab, and that page still lists the tree.
135 133 if code, codePage := f.get("/r/" + f.repo + "/code"); code != 200 || !strings.Contains(codePage, "main.go") {
136 134 t.Fatalf("code page = %d:\n%s", code, codePage)
@@ -235,7 +233,10 @@ func TestActivityFeedIncludesIssuesAndFilters(t *testing.T) {
235 233 f := newFixture(t)
236 234 f.publish(f.repo)
237 235 work := workRepo(t, map[string]string{"README.md": "# activity\n"})
238 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
236 if _, errs, err := runGit(t, work, "tag", "-a", "v1.0.0", "-m", "release notes"); err != nil {
237 t.Fatalf("tag: %s", errs)
238 }
239 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main", "refs/tags/v1.0.0"); err != nil {
239 240 t.Fatalf("main push: %s", errs)
240 241 }
241 242 issues := f.issuesRepo(t, issueFiles("01M5JR2JK29KKMAA1T9MDNTPE1", "Activity issue", "body\n"))
@@ -244,17 +245,20 @@ func TestActivityFeedIncludesIssuesAndFilters(t *testing.T) {
244 245 }
245 246
246 247 _, body := f.get("/r/" + f.repo + "/activity")
247 for _, want := range []string{"Activity issue", "opened issue", "committed"} {
248 for _, want := range []string{"Activity issue", "opened issue", "v1.0.0", "released"} {
248 249 if !strings.Contains(body, want) {
249 250 t.Fatalf("activity feed does not show %q:\n%s", want, body)
250 251 }
251 252 }
253 if strings.Contains(body, "committed") {
254 t.Fatalf("activity feed still shows commits:\n%s", body)
255 }
252 256 _, body = f.get("/r/" + f.repo + "/activity?kind=issues")
253 257 if !strings.Contains(body, "Activity issue") {
254 258 t.Fatalf("issue filter dropped the issue:\n%s", body)
255 259 }
256 if strings.Contains(body, "committed") {
257 t.Fatalf("issue filter kept a commit:\n%s", body)
260 if strings.Contains(body, "v1.0.0") {
261 t.Fatalf("issue filter kept a release:\n%s", body)
258 262 }
259 263 resp, fragment := f.htmxGet(t, "/r/"+f.repo+"/activity?kind=issues", "")
260 264 if resp.StatusCode != 200 || !strings.Contains(fragment, `id="activity-feed"`) ||