v0.8.0 eaf4295

home.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"net/http"
8	"sort"
9	"strings"
10	"time"
11
12	"github.com/a-h/templ"
13
14	"bnema.dev/ermite/internal/ports"
15)
16
17// maxRepos bounds the repositories one page lists.
18const maxRepos = 1000
19
20// pageSize is the page size asked from the commit lists: the largest the use
21// case accepts for them.
22const pageSize = 100
23
24// listRepos reads the repositories the visitor may see, ordered by name.
25// Every page of ListRepos scans the whole repository list and reads the HEAD
26// of each repository of the page, so the list is asked in one page of up to
27// maxRepos: one scan in the common case, whatever the cap of the use case is
28// (a lower one only means more pages, followed by their token).
29func (s *Server) listRepos(ctx context.Context) ([]ports.RepoView, error) {
30	var repos []ports.RepoView
31	token := ""
32	for len(repos) < maxRepos {
33		page, err := s.cfg.Repos.ListPage(ctx, principal(ctx), maxRepos, token)
34		if err != nil {
35			return nil, err
36		}
37		for _, repo := range page.Repos {
38			view, err := s.cfg.Repos.View(ctx, repo)
39			if err != nil {
40				return nil, err
41			}
42			repos = append(repos, view)
43		}
44		if token = page.NextPageToken; token == "" {
45			break
46		}
47	}
48	return repos, nil
49}
50
51// homeCols are the sortable columns of the home list, in order.
52var homeCols = []struct{ key, label, class string }{
53	{"name", "name", "c-name"},
54	{"desc", "description", "c-desc"},
55	{"push", "last push", "c-when"},
56	{"branch", "branch", "c-ref"},
57}
58
59// sortRepos orders repos by the column key; ties keep the order by name.
60func sortRepos(repos []ports.RepoView, key string, desc bool) {
61	less := func(a, b ports.RepoView) bool {
62		switch key {
63		case "desc":
64			return strings.ToLower(a.Description) < strings.ToLower(b.Description)
65		case "branch":
66			return a.DefaultBranch < b.DefaultBranch
67		case "push":
68			return a.PushedAt.Before(b.PushedAt)
69		}
70		return a.Name.String() < b.Name.String()
71	}
72	sort.SliceStable(repos, func(i, j int) bool {
73		if desc {
74			return less(repos[j], repos[i])
75		}
76		return less(repos[i], repos[j])
77	})
78}
79
80// home lists the repositories: /?sort=<col>&dir=asc|desc, newest push first
81// by default.
82func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) {
83	repos, err := s.listRepos(ctx)
84	if err != nil {
85		return nil, 0, err
86	}
87	key, desc := "push", true
88	for _, c := range homeCols {
89		if r.URL.Query().Get("sort") == c.key {
90			key, desc = c.key, c.key == "push"
91		}
92	}
93	if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" {
94		desc = d == "desc"
95	}
96	sortRepos(repos, key, desc)
97
98	now := s.cfg.Now()
99	p := HomePage{}
100	for _, c := range homeCols {
101		sc := SortCol{Label: c.label, Class: c.class, Dir: "none"}
102		next := "asc"
103		if c.key == key {
104			sc.Dir, sc.Mark = "ascending", "▲"
105			if desc {
106				sc.Dir, sc.Mark = "descending", "▼"
107			} else {
108				next = "desc"
109			}
110		}
111		sc.Href = "/?sort=" + c.key + "&dir=" + next
112		p.Cols = append(p.Cols, sc)
113	}
114	for i, repo := range repos {
115		p.Rows = append(p.Rows, homeRow(repo, now, i == 0))
116	}
117
118	l := s.base()
119	l.Title = s.instance
120	l.Live, l.LiveURL = true, liveURL("home", "")
121	l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...)
122	return Home(l, p), http.StatusOK, nil
123}
124
125// homeRow is the row of a repository in the home list.
126func homeRow(r ports.RepoView, now time.Time, cursor bool) HomeRow {
127	name := r.Name.String()
128	row := HomeRow{
129		ID: "repo-" + name, Name: name, Href: repoHref(name),
130		Desc: r.Description, Pushed: "never", Branch: r.DefaultBranch, Cursor: cursor,
131	}
132	if !r.PushedAt.IsZero() {
133		row.Pushed, row.PushedAt = ago(now, r.PushedAt), rfc3339(r.PushedAt)
134	}
135	return row
136}
137
138// repoCmds are the repositories as targets of the ":" command line, but the
139// one being viewed.
140func repoCmds(repos []ports.RepoView, except string) []CmdItem {
141	var out []CmdItem
142	for _, r := range repos {
143		if name := r.Name.String(); name != except {
144			out = append(out, CmdItem{Kind: "repo", Label: name, Href: repoHref(name)})
145		}
146	}
147	return out
148}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close