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 *http.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 *http.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 *http.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 reader, err := gzip.NewReader(r.Body)
145 if err != nil {
146 return err
147 }
148 defer reader.Close()
149 }
150
151 err := g.gitService.WriteUploadPack(r.Context(), name, reader, w)
152 if err != nil {
153 slog.Error("Error WriteUploadPack", "error", err)
154 }
155 } else if r.Method == "GET" {
156 return g.Summary(w, r)
157 }
158
159 return nil
160}
161
162func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error {
163 ext.SetHTML(w)
164 name := r.PathValue("name")
165 ref, err := g.gitService.GetHead(name)
166 if err != nil {
167 return err
168 }
169
170 tags, err := g.gitService.ListTags(name)
171 if err != nil {
172 return err
173 }
174
175 branches, err := g.gitService.ListBranches(name)
176 if err != nil {
177 return err
178 }
179
180 commits, _, err := g.gitService.ListCommits(name, "", "", 10)
181 if err != nil {
182 return err
183 }
184
185 if len(tags) > 3 {
186 tags = tags[:3]
187 }
188
189 gitList := &templates.GitItemPage{
190 Name: name,
191 Ref: ref.Name().Short(),
192 GitItemBase: &templates.GitItemSummaryPage{
193 Tags: tags,
194 Branches: branches,
195 Commits: commits,
196 },
197 }
198 templates.WritePageTemplate(w, gitList, r.Context())
199 return nil
200}
201
202func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error {
203 ext.SetHTML(w)
204 name := r.PathValue("name")
205 ref, err := g.gitService.GetHead(name)
206 if err != nil {
207 return err
208 }
209
210 file, err := g.gitService.GetAbout(name)
211 if errors.Is(err, object.ErrFileNotFound) {
212 templates.WritePageTemplate(w, &templates.GitItemPage{
213 Name: name,
214 Ref: ref.Name().Short(),
215 GitItemBase: &templates.GitItemAboutPage{
216 About: []byte("About file not configured properly"),
217 },
218 }, r.Context())
219 return nil
220 }
221 if err != nil {
222 return err
223 }
224
225 extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
226 p := parser.NewWithExtensions(extensions)
227 doc := p.Parse(file)
228
229 htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
230 opts := markdownhtml.RendererOptions{Flags: htmlFlag}
231 renderer := markdownhtml.NewRenderer(opts)
232
233 bs := markdown.Render(doc, renderer)
234
235 gitList := &templates.GitItemPage{
236 Name: name,
237 Ref: ref.Name().Short(),
238 GitItemBase: &templates.GitItemAboutPage{
239 About: bs,
240 },
241 }
242 templates.WritePageTemplate(w, gitList, r.Context())
243 return nil
244}
245
246func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error {
247 ext.SetHTML(w)
248 name := r.PathValue("name")
249
250 tags, err := g.gitService.ListTags(name)
251 if err != nil {
252 return err
253 }
254
255 branches, err := g.gitService.ListBranches(name)
256 if err != nil {
257 return err
258 }
259
260 ref, err := g.gitService.GetHead(name)
261 if err != nil {
262 return err
263 }
264
265 gitList := &templates.GitItemPage{
266 Name: name,
267 Ref: ref.Name().Short(),
268 GitItemBase: &templates.GitItemRefsPage{
269 Tags: tags,
270 Branches: branches,
271 },
272 }
273 templates.WritePageTemplate(w, gitList, r.Context())
274 return nil
275}
276
277func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error {
278 ext.SetHTML(w)
279 name := r.PathValue("name")
280 ref := r.PathValue("ref")
281 rest := r.PathValue("rest")
282 paths := []string{}
283
284 // this is avoid Split from generating a len 1 array with empty string
285 if rest != "" {
286 paths = strings.Split(rest, "/")
287 }
288
289 tree, err := g.gitService.GetTree(name, ref, rest)
290 if err != nil {
291 return err
292 }
293
294 gitList := &templates.GitItemPage{
295 Name: name,
296 Ref: ref,
297 GitItemBase: &templates.GitItemTreePage{
298 Path: paths,
299 Tree: tree,
300 },
301 }
302 templates.WritePageTemplate(w, gitList, r.Context())
303 return nil
304}
305
306func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error {
307 ext.SetHTML(w)
308 name := r.PathValue("name")
309 ref := r.PathValue("ref")
310 rest := r.PathValue("rest")
311 paths := []string{}
312
313 // this is avoid Split from generating a len 1 array with empty string
314 if rest != "" {
315 paths = strings.Split(rest, "/")
316 }
317
318 isBin, err := g.gitService.IsBinary(name, ref, rest)
319 if err != nil {
320 return err
321 }
322
323 // if it is binary no need to over all the chroma process
324 if isBin {
325 gitList := &templates.GitItemPage{
326 Name: name,
327 Ref: ref,
328 GitItemBase: &templates.GitItemBlobPage{
329 Path: paths,
330 Content: []byte("Binary file"),
331 },
332 }
333 templates.WritePageTemplate(w, gitList, r.Context())
334 return nil
335 }
336
337 file, err := g.gitService.GetFileContent(name, ref, rest)
338 if err != nil {
339 return err
340 }
341
342 filename := filepath.Base(rest)
343 lexer := GetLexers(filename)
344 style := styles.Get(g.config.GetSyntaxHighlight())
345
346 formatter := html.New(
347 html.WithLineNumbers(true),
348 html.WithLinkableLineNumbers(true, "L"),
349 )
350
351 iterator, err := lexer.Tokenise(nil, string(file))
352 if err != nil {
353 return err
354 }
355
356 var code bytes.Buffer
357 err = formatter.Format(&code, style, iterator)
358 if err != nil {
359 return err
360 }
361
362 gitList := &templates.GitItemPage{
363 Name: name,
364 Ref: ref,
365 GitItemBase: &templates.GitItemBlobPage{
366 Path: paths,
367 Content: code.Bytes(),
368 },
369 }
370 templates.WritePageTemplate(w, gitList, r.Context())
371 return nil
372}
373
374func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error {
375 ext.SetHTML(w)
376 name := r.PathValue("name")
377 ref := r.PathValue("ref")
378 from := r.URL.Query().Get("from")
379
380 commits, next, err := g.gitService.ListCommits(name, ref, from, 100)
381 if err != nil {
382 return err
383 }
384
385 gitList := &templates.GitItemPage{
386 Name: name,
387 Ref: ref,
388 GitItemBase: &templates.GitItemLogPage{
389 Commits: commits,
390 Next: next,
391 },
392 }
393 templates.WritePageTemplate(w, gitList, r.Context())
394 return nil
395}
396
397func (g *GitHandler) Ref(w http.ResponseWriter, r *http.Request) error {
398 ext.SetHTML(w)
399 name := r.PathValue("name")
400 ref := r.PathValue("ref")
401
402 commit, tag, err := g.gitService.GetTag(ref, name)
403 if err != nil {
404 return err
405 }
406
407 gitList := &templates.GitItemPage{
408 Name: name,
409 Ref: ref,
410 GitItemBase: &templates.GitItemRefPage{
411 Commit: commit,
412 Reference: tag,
413 },
414 }
415 templates.WritePageTemplate(w, gitList, r.Context())
416 return nil
417}
418
419func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error {
420 ext.SetHTML(w)
421 name := r.PathValue("name")
422 ref := r.PathValue("ref")
423
424 commit, err := g.gitService.LastCommit(name, ref)
425 if err != nil {
426 return err
427 }
428
429 diff, err := g.gitService.Diff(name, ref)
430 if err != nil {
431 return err
432 }
433
434 lexer := lexers.Get("diff")
435 style := styles.Get(g.config.GetSyntaxHighlight())
436
437 formatter := html.New(
438 html.WithLineNumbers(true),
439 html.WithLinkableLineNumbers(true, "L"),
440 )
441
442 iterator, err := lexer.Tokenise(nil, diff)
443 if err != nil {
444 return err
445 }
446
447 var code bytes.Buffer
448 err = formatter.Format(&code, style, iterator)
449 if err != nil {
450 return err
451 }
452
453 gitList := &templates.GitItemPage{
454 Name: name,
455 Ref: ref,
456 GitItemBase: &templates.GitItemCommitPage{
457 Commit: commit,
458 Diff: code.Bytes(),
459 },
460 }
461 templates.WritePageTemplate(w, gitList, r.Context())
462 return nil
463}
464
465func GetLexers(filename string) chroma.Lexer {
466 if filename == "APKBUILD" {
467 return lexers.Get("sh")
468 }
469
470 if strings.HasSuffix(filename, ".qtpl") {
471 return lexers.Get("html")
472 }
473
474 lexer := lexers.Get(filename)
475
476 if lexer == nil {
477 lexer = lexers.Get("txt")
478 }
479 return lexer
480}
481
482func isPublic(r *service.Repository) bool {
483 return r.Public
484}
485
486func orderBy(repos []*service.Repository, order config.OrderBy) []*service.Repository {
487 switch order {
488 case config.AlphabeticalAsc:
489 sort.Slice(repos, func(i, j int) bool {
490 return repos[i].Name < repos[j].Name
491 })
492 case config.AlphabeticalDesc:
493 sort.Slice(repos, func(i, j int) bool {
494 return repos[i].Name > repos[j].Name
495 })
496 case config.LastCommitAsc:
497 sort.Slice(repos, func(i, j int) bool {
498 return repos[i].LastCommit.Commit().Committer.When.Before(repos[j].LastCommit.Commit().Committer.When)
499 })
500 case config.LastCommitDesc:
501 sort.Slice(repos, func(i, j int) bool {
502 return repos[i].LastCommit.Commit().Committer.When.After(repos[j].LastCommit.Commit().Committer.When)
503 })
504 }
505
506 return repos
507}