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 cuts a file the use case opened into rows of the diff; no rule is decided here
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}