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