v0.36.0 1fb2421

context.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"net/http"
8	"strconv"
9	"strings"
10
11	"github.com/a-h/templ"
12
13	"bnema.dev/ermite/internal/domain"
14)
15
16// maxContextLines bounds the lines one gap loads: the rest stays behind a
17// smaller gap that loads the next lines.
18const maxContextLines = 2000
19
20// ContextRows is the hidden lines of a gap, for both layouts: they replace the
21// rows of the gap. More is the gap that is left, when the file goes on.
22type ContextRows struct {
23	Lines []DiffLine
24	More  *DiffGap
25}
26
27// diffContext is GET /<repo>/context/<commit>?path=&old=&new=&n=: the lines of
28// a file of a diff that its hunks hide, as rows of the diff tables. commit is
29// the full id of the commit of the new side; path the file there; old and new
30// the numbers of the first hidden line on each side, which move together (the
31// lines are unchanged); n the number of lines, 0 for the rest of the file.
32//
33//hexcheck:ignore suspicious-business-logic-in-adapter validates diff-context HTTP parameters, rejects binary or oversized files, and caps rendered context rows
34func (s *Server) diffContext(ctx context.Context, r *http.Request) (templ.Component, int, error) {
35	sha := r.PathValue("sha")
36	if !isFullID(sha) {
37		return nil, 0, errNotFound
38	}
39	q := r.URL.Query()
40	num := func(k string, least int) (int, bool) {
41		v, err := strconv.Atoi(q.Get(k))
42		return v, err == nil && v >= least && strconv.Itoa(v) == q.Get(k)
43	}
44	oldFrom, ok1 := num("old", 1)
45	newFrom, ok2 := num("new", 1)
46	n, ok3 := num("n", 0)
47	path := q.Get("path")
48	if !ok1 || !ok2 || !ok3 || path == "" {
49		return nil, 0, errNotFound
50	}
51	repo, err := domain.ParseRepoName(r.PathValue("repo"))
52	if err != nil {
53		return nil, 0, err
54	}
55	info, body, err := s.openBlobAt(ctx, repo, sha, path)
56	if err != nil {
57		return nil, 0, err
58	}
59	b := &blobStream{body: body, info: info}
60	defer b.close()
61	if info.Binary || info.Size > maxCodeBytes {
62		return nil, 0, errNotFound
63	}
64	if err := b.fill(int(info.Size)); err != nil {
65		return nil, 0, err
66	}
67	// Lines as Git numbers them: split on LF only (a lone CR is a character of
68	// its line, shown as a marker; the CR of a CRLF is dropped by segments).
69	text := strings.ToValidUTF8(string(b.buf), "\uFFFD")
70	lines := strings.Split(text, "\n")
71	noEOL := text != "" && !strings.HasSuffix(text, "\n")
72	if !noEOL {
73		lines = lines[:len(lines)-1]
74	}
75	if newFrom > len(lines)+1 {
76		return nil, 0, errNotFound
77	}
78	end := len(lines)
79	if n > 0 {
80		end = min(end, newFrom-1+n)
81	}
82	cut := min(end, newFrom-1+maxContextLines)
83
84	// The colours of the lines come from the file lexed down to the last of
85	// them; plain where that is not possible (limits, no language).
86	colours := s.hl.highlightContext(ctx, info.ID.String(), path, lines, newFrom-1, cut)
87	var out ContextRows
88	for i := newFrom - 1; i < cut; i++ {
89		o, nw := oldFrom+i-(newFrom-1), i+1
90		segs := segments(lines[i])
91		if colours != nil && colours[i-(newFrom-1)] != nil {
92			segs = colours[i-(newFrom-1)]
93		}
94		out.Lines = append(out.Lines, DiffLine{
95			Kind: "ctx", Mark: " ", Segs: segs,
96			Old: itoa(o), New: itoa(nw), oldN: o, newN: nw,
97			NoEOL: noEOL && i == len(lines)-1,
98		})
99	}
100	if cut < end {
101		rest := 0
102		if n > 0 {
103			rest = end - cut
104		}
105		next := gapHref(r, oldFrom+cut-(newFrom-1), cut+1, rest)
106		out.More = &DiffGap{Key: q.Get("key"), Lines: rest, Src: next}
107	}
108	return DiffContext(out), http.StatusOK, nil
109}
110
111// gapHref is the address of the gap that starts at the lines old and new.
112func gapHref(r *http.Request, old, new, n int) string {
113	q := r.URL.Query()
114	q.Set("old", itoa(old))
115	q.Set("new", itoa(new))
116	q.Set("n", itoa(n))
117	return r.URL.Path + "?" + q.Encode()
118}

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 4

Tags 45

↑↓ moveenter openesc close