main 1fb2421

commits.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"net/http"
8	"net/url"
9	"path"
10
11	"github.com/a-h/templ"
12
13	"bnema.dev/ermite/internal/domain"
14)
15
16// commitsPerPage is how many commits one page of history lists.
17const commitsPerPage = 50
18
19// DayGroup is the commits of one day (UTC, by committer date).
20type DayGroup struct {
21	Day  string
22	Rows []CommitRow
23}
24
25// CommitsPage is the history of a ref, or of a path at a ref.
26type CommitsPage struct {
27	Ref string
28	// Path is the path the history is limited to, ready to display; "" for
29	// the whole history.
30	Path  string
31	Days  []DayGroup
32	Count int
33	// Older is the link to the next page, "" on the last page. Newest is the
34	// link back to the first page, "" on the first page: page tokens only go
35	// forward.
36	Older, Newest string
37	// Open links to the file or directory at this ref, when it exists there.
38	Open *Link
39	// Refs are the branch heads and tags among the commits of this page.
40	Refs []Link
41	// Live is true when the page announces new commits of its branch.
42	Live bool
43}
44
45// commits lists the history of a ref, newest first, a page at a time, with
46// an optional path to limit it to. The page token of the use case travels in the
47// "page" query parameter.
48func (s *Server) commits(ctx context.Context, r *http.Request) (templ.Component, int, error) {
49	d, err := s.loadRepo(ctx, r.PathValue("repo"))
50	if err != nil {
51		return nil, 0, err
52	}
53	sel, ok := d.locate(r.PathValue("rest"))
54	if !ok {
55		return nil, 0, errNotFound
56	}
57	token := r.URL.Query().Get("page")
58	res, err := s.commitList(ctx, d.repo.Name, sel.rev, sel.path, commitsPerPage, token)
59	if err != nil {
60		return nil, 0, err
61	}
62	cs := res.Commits
63	if len(cs) == 0 && token == "" && sel.path != "" {
64		// Git answers an empty history for a path that never existed.
65		return nil, 0, errNotFound
66	}
67
68	p := CommitsPage{Ref: sel.name, Path: visibleText(sel.path), Count: len(cs)}
69	base := repoHref(d.name(), "commits", sel.name, sel.path)
70	if t := res.NextPageToken; t != "" {
71		p.Older = base + "?page=" + url.QueryEscape(t)
72	}
73	if token != "" {
74		p.Newest = base
75	}
76	s.fillDays(ctx, &p, d, cs)
77	if sel.path != "" && token == "" {
78		p.Open = s.pathLink(ctx, d, sel)
79	}
80
81	l := s.repoLayout(ctx, d, "commits")
82	pinRef(&l, sel)
83	if d.isBranchHead(sel) && token == "" && sel.path == "" && len(cs) > 0 {
84		// The first page of a branch announces the commits pushed to it.
85		p.Live = true
86		l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=commits&ref="+url.QueryEscape(sel.name)+"&head="+sel.rev)
87	}
88	l.Crumbs = pathCrumbs(d.name(), "commits", sel)
89	if sel.path == "" {
90		l.Title = "commits · " + d.name() + " · " + s.instance
91	} else {
92		l.Title = "history of " + visibleText(sel.path) + " · " + d.name() + " · " + s.instance
93	}
94	l.HasRail = true
95	return Commits(l, p), http.StatusOK, nil
96}
97
98// maxCheckLookups bounds the commits of one page whose checks are read: each
99// is one read of the store.
100const maxCheckLookups = 100
101
102// fillDays groups the commits of a page by day and lists the refs among them.
103func (s *Server) fillDays(ctx context.Context, p *CommitsPage, d *repoData, cs []domain.Commit) {
104	now := s.cfg.Now()
105	branchesAt, tagsAt := namesAt(d.branches), namesAt(d.tags)
106	for i, c := range cs {
107		row := commitRow(d.name(), c, now)
108		row.ID, row.Cursor = "c-"+row.Short, i == 0
109		row.Branches, row.Tags = branchesAt[c.ID.String()], tagsAt[c.ID.String()]
110		if i < maxCheckLookups {
111			row.Checks = checksSummary(d.repo.Name, c.ID.String(), s.checksFor(ctx, d.repo.Name, c.ID.String()))
112		}
113		day := "unknown date"
114		if t := c.Committer.Time; !t.IsZero() {
115			day = t.UTC().Format("2006-01-02")
116		}
117		if n := len(p.Days); n == 0 || p.Days[n-1].Day != day {
118			p.Days = append(p.Days, DayGroup{Day: day})
119		}
120		g := &p.Days[len(p.Days)-1]
121		g.Rows = append(g.Rows, row)
122		for _, t := range row.Tags {
123			p.Refs = append(p.Refs, Link{Label: "tag " + t, Href: "#" + row.ID})
124		}
125		for _, b := range row.Branches {
126			p.Refs = append(p.Refs, Link{Label: "branch " + b, Href: "#" + row.ID})
127		}
128	}
129}
130
131// pathLink finds what the path of a history is at the ref: a directory
132// (tree) or a file (blob). It is nil when the path is not there, a deleted
133// file for one, or when the listing fails: the link is a convenience.
134func (s *Server) pathLink(ctx context.Context, d *repoData, sel target) *Link {
135	dir := path.Dir(sel.path)
136	if dir == "." {
137		dir = ""
138	}
139	tree, err := s.getTree(ctx, d.repo.Name, sel.rev, dir, false)
140	if err != nil {
141		return nil
142	}
143	for _, e := range tree.Entries {
144		if e.Path != sel.path {
145			continue
146		}
147		if e.Kind == domain.EntryDirectory {
148			return &Link{Label: "open", Href: repoHref(d.name(), "tree", sel.name, sel.path)}
149		}
150		return &Link{Label: "open", Href: repoHref(d.name(), "blob", sel.name, sel.path)}
151	}
152	return nil
153}

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