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