1package git
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "log/slog"
9 "net/http"
10 "os"
11 "path/filepath"
12 "sort"
13 "strings"
14
15 "git.gabrielgio.me/cerrado/pkg/config"
16 "git.gabrielgio.me/cerrado/pkg/ext"
17 "git.gabrielgio.me/cerrado/pkg/service"
18 "git.gabrielgio.me/cerrado/pkg/u"
19 "git.gabrielgio.me/cerrado/templates"
20 "github.com/alecthomas/chroma/v2"
21 "github.com/alecthomas/chroma/v2/formatters/html"
22 "github.com/alecthomas/chroma/v2/lexers"
23 "github.com/alecthomas/chroma/v2/styles"
24 "github.com/go-git/go-git/v5/plumbing/object"
25 "github.com/gomarkdown/markdown"
26 markdownhtml "github.com/gomarkdown/markdown/html"
27 "github.com/gomarkdown/markdown/parser"
28)
29
30type (
31 GitHandler struct {
32 gitService *service.GitService
33 config configurationRepository
34 }
35
36 configurationRepository interface {
37 GetRootReadme() string
38 GetSyntaxHighlight() string
39 GetOrderBy() config.OrderBy
40 }
41)
42
43func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler {
44 return &GitHandler{
45 gitService: gitService,
46 config: confRepo,
47 }
48}
49
50func (g *GitHandler) List(w http.ResponseWriter, r *http.Request) error {
51 // this is the only handler that needs to handle authentication itself.
52 // everything else relay on name path parameter
53 logged := ext.IsLoggedIn(r.Context())
54
55 repos, err := g.gitService.ListRepositories()
56 if err != nil {
57 return err
58 }
59
60 if !logged {
61 repos = u.Filter(repos, isPublic)
62 }
63
64 f, err := os.Open(g.config.GetRootReadme())
65 if err != nil {
66 return err
67 }
68
69 bs, err := io.ReadAll(f)
70 if err != nil {
71 return err
72 }
73
74 extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
75 p := parser.NewWithExtensions(extensions)
76 doc := p.Parse(bs)
77
78 htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
79 opts := markdownhtml.RendererOptions{Flags: htmlFlag}
80 renderer := markdownhtml.NewRenderer(opts)
81
82 bs = markdown.Render(doc, renderer)
83
84 gitList := &templates.GitListPage{
85 Respositories: orderBy(repos, g.config.GetOrderBy()),
86 About: bs,
87 }
88 templates.WritePageTemplate(w, gitList, r.Context())
89 return nil
90}
91
92func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error {
93 ext.SetGZip(w)
94 name := r.PathValue("name")
95 file := r.PathValue("file")
96 ref := strings.TrimSuffix(file, ".tar.gz")
97
98 // TODO: remove it once we can support more than gzip
99 if !strings.HasSuffix(file, ".tar.gz") {
100 ext.NotFound(w, r)
101 return nil
102 }
103
104 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
105 ext.SetFileName(w, filename)
106
107 prefix := fmt.Sprintf("%s-%s", name, ref)
108 err := g.gitService.WriteTarGZip(w, name, ref, prefix)
109 if err != nil {
110 // once we start writing to the body we can't report error anymore
111 // so we are only left with printing the error.
112 slog.Error("Error generating tar gzip file", "error", err)
113 }
114
115 return nil
116}
117
118func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error {
119 ext.SetHTML(w)
120 name := r.PathValue("name")
121 ref, err := g.gitService.GetHead(name)
122 if err != nil {
123 return err
124 }
125
126 tags, err := g.gitService.ListTags(name)
127 if err != nil {
128 return err
129 }
130
131 branches, err := g.gitService.ListBranches(name)
132 if err != nil {
133 return err
134 }
135
136 commits, _, err := g.gitService.ListCommits(name, "", "", 10)
137 if err != nil {
138 return err
139 }
140
141 gitList := &templates.GitItemPage{
142 Name: name,
143 Ref: ref.Name().Short(),
144 GitItemBase: &templates.GitItemSummaryPage{
145 Tags: tags,
146 Branches: branches,
147 Commits: commits,
148 },
149 }
150 templates.WritePageTemplate(w, gitList, r.Context())
151 return nil
152}
153
154func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error {
155 ext.SetHTML(w)
156 name := r.PathValue("name")
157 ref, err := g.gitService.GetHead(name)
158 if err != nil {
159 return err
160 }
161
162 file, err := g.gitService.GetAbout(name)
163 if errors.Is(err, object.ErrFileNotFound) {
164 templates.WritePageTemplate(w, &templates.GitItemPage{
165 Name: name,
166 Ref: ref.Name().Short(),
167 GitItemBase: &templates.GitItemAboutPage{
168 About: []byte("About file not configured properly"),
169 },
170 }, r.Context())
171 return nil
172 }
173 if err != nil {
174 return err
175 }
176
177 extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
178 p := parser.NewWithExtensions(extensions)
179 doc := p.Parse(file)
180
181 htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
182 opts := markdownhtml.RendererOptions{Flags: htmlFlag}
183 renderer := markdownhtml.NewRenderer(opts)
184
185 bs := markdown.Render(doc, renderer)
186
187 gitList := &templates.GitItemPage{
188 Name: name,
189 Ref: ref.Name().Short(),
190 GitItemBase: &templates.GitItemAboutPage{
191 About: bs,
192 },
193 }
194 templates.WritePageTemplate(w, gitList, r.Context())
195 return nil
196}
197
198func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error {
199 ext.SetHTML(w)
200 name := r.PathValue("name")
201
202 tags, err := g.gitService.ListTags(name)
203 if err != nil {
204 return err
205 }
206
207 branches, err := g.gitService.ListBranches(name)
208 if err != nil {
209 return err
210 }
211
212 ref, err := g.gitService.GetHead(name)
213 if err != nil {
214 return err
215 }
216
217 gitList := &templates.GitItemPage{
218 Name: name,
219 Ref: ref.Name().Short(),
220 GitItemBase: &templates.GitItemRefsPage{
221 Tags: tags,
222 Branches: branches,
223 },
224 }
225 templates.WritePageTemplate(w, gitList, r.Context())
226 return nil
227}
228
229func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error {
230 ext.SetHTML(w)
231 name := r.PathValue("name")
232 ref := r.PathValue("ref")
233 rest := r.PathValue("rest")
234 paths := []string{}
235
236 // this is avoid Split from generating a len 1 array with empty string
237 if rest != "" {
238 paths = strings.Split(rest, "/")
239 }
240
241 tree, err := g.gitService.GetTree(name, ref, rest)
242 if err != nil {
243 return err
244 }
245
246 gitList := &templates.GitItemPage{
247 Name: name,
248 Ref: ref,
249 GitItemBase: &templates.GitItemTreePage{
250 Path: paths,
251 Tree: tree,
252 },
253 }
254 templates.WritePageTemplate(w, gitList, r.Context())
255 return nil
256}
257
258func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error {
259 ext.SetHTML(w)
260 name := r.PathValue("name")
261 ref := r.PathValue("ref")
262 rest := r.PathValue("rest")
263 paths := []string{}
264
265 // this is avoid Split from generating a len 1 array with empty string
266 if rest != "" {
267 paths = strings.Split(rest, "/")
268 }
269
270 isBin, err := g.gitService.IsBinary(name, ref, rest)
271 if err != nil {
272 return err
273 }
274
275 // if it is binary no need to over all the chroma process
276 if isBin {
277 gitList := &templates.GitItemPage{
278 Name: name,
279 Ref: ref,
280 GitItemBase: &templates.GitItemBlobPage{
281 Path: paths,
282 Content: []byte("Binary file"),
283 },
284 }
285 templates.WritePageTemplate(w, gitList, r.Context())
286 return nil
287 }
288
289 file, err := g.gitService.GetFileContent(name, ref, rest)
290 if err != nil {
291 return err
292 }
293
294 filename := filepath.Base(rest)
295 lexer := GetLexers(filename)
296 style := styles.Get(g.config.GetSyntaxHighlight())
297
298 formatter := html.New(
299 html.WithLineNumbers(true),
300 html.WithLinkableLineNumbers(true, "L"),
301 )
302
303 iterator, err := lexer.Tokenise(nil, string(file))
304 if err != nil {
305 return err
306 }
307
308 var code bytes.Buffer
309 err = formatter.Format(&code, style, iterator)
310 if err != nil {
311 return err
312 }
313
314 gitList := &templates.GitItemPage{
315 Name: name,
316 Ref: ref,
317 GitItemBase: &templates.GitItemBlobPage{
318 Path: paths,
319 Content: code.Bytes(),
320 },
321 }
322 templates.WritePageTemplate(w, gitList, r.Context())
323 return nil
324}
325
326func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error {
327 ext.SetHTML(w)
328 name := r.PathValue("name")
329 ref := r.PathValue("ref")
330 from := r.URL.Query().Get("from")
331
332 commits, next, err := g.gitService.ListCommits(name, ref, from, 100)
333 if err != nil {
334 return err
335 }
336
337 gitList := &templates.GitItemPage{
338 Name: name,
339 Ref: ref,
340 GitItemBase: &templates.GitItemLogPage{
341 Commits: commits,
342 Next: next,
343 },
344 }
345 templates.WritePageTemplate(w, gitList, r.Context())
346 return nil
347}
348
349func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error {
350 ext.SetHTML(w)
351 name := r.PathValue("name")
352 ref := r.PathValue("ref")
353
354 commit, err := g.gitService.LastCommit(name, ref)
355 if err != nil {
356 return err
357 }
358
359 diff, err := g.gitService.Diff(name, ref)
360 if err != nil {
361 return err
362 }
363
364 gitList := &templates.GitItemPage{
365 Name: name,
366 Ref: ref,
367 GitItemBase: &templates.GitItemCommitPage{
368 Commit: commit,
369 Diff: diff,
370 },
371 }
372 templates.WritePageTemplate(w, gitList, r.Context())
373 return nil
374}
375
376func GetLexers(filename string) chroma.Lexer {
377 if filename == "APKBUILD" {
378 return lexers.Get("sh")
379 }
380
381 if strings.HasSuffix(filename, ".qtpl") {
382 return lexers.Get("html")
383 }
384
385 lexer := lexers.Get(filename)
386
387 if lexer == nil {
388 lexer = lexers.Get("txt")
389 }
390 return lexer
391}
392
393func isPublic(r *service.Repository) bool {
394 return r.Public
395}
396
397func orderBy(repos []*service.Repository, order config.OrderBy) []*service.Repository {
398 switch order {
399 case config.AlphabeticalAsc:
400 sort.Slice(repos, func(i, j int) bool {
401 return repos[i].Name < repos[j].Name
402 })
403 case config.AlphabeticalDesc:
404 sort.Slice(repos, func(i, j int) bool {
405 return repos[i].Name > repos[j].Name
406 })
407 case config.LastCommitAsc:
408 sort.Slice(repos, func(i, j int) bool {
409 return repos[i].LastCommit.Committer.When.Before(repos[j].LastCommit.Committer.When)
410 })
411 case config.LastCommitDesc:
412 sort.Slice(repos, func(i, j int) bool {
413 return repos[i].LastCommit.Committer.When.After(repos[j].LastCommit.Committer.When)
414 })
415 }
416
417 return repos
418}