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/web/activity.go +44−51

@@ -1,10 +1,11 @@
1 1 package web
2 2
3 // The repository activity feed. It is derived, never stored: commits come from
4 // git, issues and changes from the metadata branch, releases from tags, and runs
5 // from the queue. That means there is no notification table or unread state to
6 // keep in sync, and a private repository's feed follows the same read rule as
7 // every other page.
3 // The repository activity feed. It is derived, never stored: issues, changes,
4 // comments, and state changes come from the metadata branch; releases come from
5 // tags. Commits and CI runs are deliberately not events: the feed is for things
6 // people said and shipped. That means there is no notification table or unread
7 // state to keep in sync, and a private repository's feed follows the same read
8 // rule as every other page.
8 9
9 10 import (
10 11 "net/http"
@@ -13,6 +14,7 @@ import (
13 14 "strings"
14 15 "time"
15 16
17 "github.com/rodent-software/vole/core/git"
16 18 "github.com/rodent-software/vole/forge/repo"
17 19 "github.com/rodent-software/vole/metadata"
18 20 )
@@ -20,16 +22,14 @@ import (
20 22 const activityLimit = 100
21 23
22 24 type activityEvent struct {
23 Kind string
24 Icon string
25 Time time.Time
26 Actor string
27 ActorName string
28 Title string
29 Detail string
30 URL string
31 Badge string
32 BadgeClass string
25 Kind string
26 Icon string
27 Time time.Time
28 Actor string
29 ActorName string
30 Title string
31 Detail string
32 URL string
33 33 // Assignees is kept on issue events so a filter can narrow the feed to
34 34 // "things assigned to me" without changing the event source.
35 35 Assignees []string
@@ -46,6 +46,13 @@ type activityActor struct {
46 46 type activityFeed struct {
47 47 Events []activityEvent
48 48 Assignees []string
49 // Summary fields describe the repository itself rather than the feed. The
50 // landing page builds its cards from the same read, so listing a repository
51 // costs one pass instead of one for the card and another for the events.
52 LastCommit *git.Commit
53 LastTag *git.Tag
54 OpenIssues int
55 OpenChanges int
49 56 }
50 57
51 58 type activityView struct {
@@ -61,16 +68,12 @@ type activityView struct {
61 68
62 69 func normalizeActivityKind(kind string) string {
63 70 switch strings.ToLower(strings.TrimSpace(kind)) {
64 case "commit", "commits":
65 return "commit"
66 71 case "issue", "issues":
67 72 return "issue"
68 73 case "change", "changes":
69 74 return "change"
70 75 case "release", "releases":
71 76 return "release"
72 case "run", "runs":
73 return "run"
74 77 }
75 78 return ""
76 79 }
@@ -78,6 +81,9 @@ func normalizeActivityKind(kind string) string {
78 81 func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityFeed {
79 82 g := m.Git()
80 83 var events []activityEvent
84 var lastCommit *git.Commit
85 var lastTag *git.Tag
86 openIssues, openChanges := 0, 0
81 87
82 88 var stateChanges []metadata.StateChange
83 89 if kind == "" || kind == "issue" || kind == "change" {
@@ -86,17 +92,12 @@ func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityF
86 92 issuesByID := map[string]*metadata.Issue{}
87 93 requestsByID := map[string]*metadata.Request{}
88 94
89 if kind == "" || kind == "commit" {
95 // The landing page card still needs the head commit; commits themselves are
96 // not feed events, so only the newest one is read.
97 if kind == "" {
90 98 if commit, _ := g.RevParse(m.DefaultBranch); commit != "" {
91 if commits, err := g.Log(commit, 20); err == nil {
92 for _, c := range commits {
93 events = append(events, activityEvent{
94 Kind: "commit", Icon: "git-commit-horizontal",
95 Time: time.Unix(c.When, 0).UTC(), Actor: c.Author, ActorName: c.Author,
96 Title: c.Subject, Detail: "committed",
97 URL: "/r/" + m.Name + "/commit/" + c.SHA,
98 })
99 }
99 if commits, err := g.Log(commit, 1); err == nil && len(commits) > 0 {
100 lastCommit = &commits[0]
100 101 }
101 102 }
102 103 }
@@ -104,6 +105,9 @@ func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityF
104 105 if kind == "" || kind == "issue" {
105 106 if issues, err := metadata.LoadIssues(g, metadata.Ref); err == nil {
106 107 for _, iss := range issues {
108 if iss.Open() {
109 openIssues++
110 }
107 111 issuesByID[iss.ID] = iss
108 112 actor := w.who(iss.Author)
109 113 events = append(events, activityEvent{
@@ -128,6 +132,9 @@ func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityF
128 132 if kind == "" || kind == "change" {
129 133 if requests, err := metadata.LoadRequests(g, metadata.Ref); err == nil {
130 134 for _, req := range requests {
135 if req.Open() {
136 openChanges++
137 }
131 138 requestsByID[req.ID] = req
132 139 events = append(events, activityEvent{
133 140 Kind: "change", Icon: "git-pull-request", Time: req.Created,
@@ -190,6 +197,9 @@ func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityF
190 197
191 198 if kind == "" || kind == "release" {
192 199 if tags, err := g.Tags(); err == nil {
200 if len(tags) > 0 {
201 lastTag = &tags[0]
202 }
193 203 for _, t := range tags {
194 204 events = append(events, activityEvent{
195 205 Kind: "release", Icon: "tag", Time: time.Unix(t.When, 0).UTC(),
@@ -200,27 +210,6 @@ func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityF
200 210 }
201 211 }
202 212
203 if (kind == "" || kind == "run") && w.Runs != nil {
204 if runs, err := w.Runs.Recent(m.Name, 20); err == nil {
205 for _, run := range runs {
206 at := run.Finished
207 if at.IsZero() {
208 at = run.Created
209 }
210 ref := run.Branch
211 if run.Tag != "" {
212 ref = "tag " + run.Tag
213 }
214 events = append(events, activityEvent{
215 Kind: "run", Icon: "play", Time: at,
216 Title: run.Job, Detail: run.Workflow + " · " + ref,
217 URL: "/r/" + m.Name + "/runs/" + run.ID,
218 Badge: run.Status, BadgeClass: run.Status,
219 })
220 }
221 }
222 }
223
224 213 sort.SliceStable(events, func(i, j int) bool {
225 214 return events[i].Time.After(events[j].Time)
226 215 })
@@ -241,7 +230,11 @@ func (w *Web) repoActivity(m *repo.Meta, kind, actor, assignee string) activityF
241 230 if len(events) > activityLimit {
242 231 events = events[:activityLimit]
243 232 }
244 return activityFeed{Events: events, Assignees: assignees}
233 return activityFeed{
234 Events: events, Assignees: assignees,
235 LastCommit: lastCommit, LastTag: lastTag,
236 OpenIssues: openIssues, OpenChanges: openChanges,
237 }
245 238 }
246 239
247 240 func activityAssignees(events []activityEvent) []string {