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/web.go +37−67

@@ -131,6 +131,7 @@ func (w *Web) repoNav(name, tab string, canAdmin bool) repoNavData {
131 131 if err != nil {
132 132 return n
133 133 }
134 n.Description = m.Description
134 135 if acl, err := m.ACL(); err == nil {
135 136 n.Private = !acl.Can("", access.Read)
136 137 }
@@ -138,10 +139,11 @@ func (w *Web) repoNav(name, tab string, canAdmin bool) repoNavData {
138 139 }
139 140
140 141 type repoNavData struct {
141 Name string
142 Tab string
143 Private bool
144 CanAdmin bool
142 Name string
143 Description string
144 Tab string
145 Private bool
146 CanAdmin bool
145 147 }
146 148
147 149 // Page is embedded by every view. Exported so html/template can promote its
@@ -167,6 +169,28 @@ type Page struct {
167 169 // Query is the repository search on the index page. The global search box
168 170 // echoes it; other pages leave it empty.
169 171 Query string
172 // Section is the global destination the current page belongs to, used by the
173 // rail to mark the current item. It is presentation only.
174 Section string
175 }
176
177 // pageSection maps a request path to the global destination it belongs to. A
178 // repository page belongs to Repositories: the repository's own tabs carry the
179 // finer context.
180 func pageSection(path string) string {
181 switch {
182 case path == "/" || strings.HasPrefix(path, "/r/"):
183 return "repos"
184 case strings.HasPrefix(path, "/runners"):
185 return "runners"
186 case strings.HasPrefix(path, "/activity"):
187 return "activity"
188 case strings.HasPrefix(path, "/admin"):
189 return "admin"
190 case strings.HasPrefix(path, "/account"):
191 return "account"
192 }
193 return ""
170 194 }
171 195
172 196 // canRead reports whether the viewer may see the repository. An anonymous
@@ -224,67 +248,6 @@ func (w *Web) render(rw http.ResponseWriter, name string, data any, status int)
224 248
225 249 // ---------- handlers ----------
226 250
227 func (w *Web) IndexPage(rw http.ResponseWriter, r *http.Request) {
228 entries, err := w.Index.List()
229 if err != nil {
230 w.serverError(rw, r, "index unavailable: "+err.Error())
231 return
232 }
233 query := strings.TrimSpace(r.URL.Query().Get("q"))
234 needle := strings.ToLower(query)
235 var visible []index.Entry
236 for _, e := range entries {
237 m, err := w.Repos.Open(e.Name)
238 if err != nil {
239 continue
240 }
241 if !w.canRead(r, m) {
242 continue
243 }
244 if needle != "" &&
245 !strings.Contains(strings.ToLower(e.Name), needle) &&
246 !strings.Contains(strings.ToLower(e.Description), needle) {
247 continue
248 }
249 visible = append(visible, e)
250 }
251 page := w.page(r, "Repositories")
252 page.Query = query
253 var events []activityEvent
254 if query == "" {
255 events = w.instanceActivity(visible)
256 }
257 w.render(rw, "index", struct {
258 Page
259 Repos []index.Entry
260 Query string
261 Events []activityEvent
262 }{page, visible, query, events}, 0)
263 }
264
265 // instanceActivity merges the visible repositories' feeds. It is intentionally
266 // bounded after the merge: a single-tenant instance has few repositories, and a
267 // per-repository cap would hide a quiet repository's only event.
268 func (w *Web) instanceActivity(entries []index.Entry) []activityEvent {
269 const limit = 50
270 var events []activityEvent
271 for _, e := range entries {
272 m, err := w.Repos.Open(e.Name)
273 if err != nil {
274 continue
275 }
276 for _, ev := range w.repoActivity(m, "", "", "").Events {
277 ev.Repo = m.Name
278 events = append(events, ev)
279 }
280 }
281 sort.SliceStable(events, func(i, j int) bool { return events[i].Time.After(events[j].Time) })
282 if len(events) > limit {
283 events = events[:limit]
284 }
285 return events
286 }
287
288 251 // open resolves a repository for the viewer, answering 404 when it is missing
289 252 // or not readable. A private repository is indistinguishable from a missing one.
290 253 func (w *Web) open(rw http.ResponseWriter, r *http.Request) (*repo.Meta, bool) {
@@ -524,7 +487,10 @@ func (w *Web) Commits(rw http.ResponseWriter, r *http.Request) {
524 487
525 488 // ---------- helpers ----------
526 489
527 func cloneURL(r *http.Request, name string) string {
490 // cloneBase is the read-only HTTP transport root for this instance, as the
491 // browser reached it. It is what the landing page shows an anonymous visitor
492 // who wants to clone without an account.
493 func cloneBase(r *http.Request) string {
528 494 scheme := "http"
529 495 if r.TLS != nil {
530 496 scheme = "https"
@@ -532,7 +498,11 @@ func cloneURL(r *http.Request, name string) string {
532 498 if p := r.Header.Get("X-Forwarded-Proto"); p != "" {
533 499 scheme = p
534 500 }
535 return fmt.Sprintf("%s://%s/git/%s.git", scheme, r.Host, name)
501 return fmt.Sprintf("%s://%s/git/", scheme, r.Host)
502 }
503
504 func cloneURL(r *http.Request, name string) string {
505 return cloneBase(r) + name + ".git"
536 506 }
537 507
538 508 func sshURL(addr, name string) string {