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) Commit(w http.ResponseWriter, r *http.Request) error {
354 ext.SetHTML(w)
355 name := r.PathValue("name")
356 ref := r.PathValue("ref")
357
358 commit, err := g.gitService.LastCommit(name, ref)
359 if err != nil {
360 return err
361 }
362
363 diff, err := g.gitService.Diff(name, ref)
364 if err != nil {
365 return err
366 }
367
368 lexer := lexers.Get("diff")
369 style := styles.Get(g.config.GetSyntaxHighlight())
370
371 formatter := html.New(
372 html.WithLineNumbers(true),
373 html.WithLinkableLineNumbers(true, "L"),
374 )
375
376 iterator, err := lexer.Tokenise(nil, diff)
377 if err != nil {
378 return err
379 }
380
381 var code bytes.Buffer
382 err = formatter.Format(&code, style, iterator)
383 if err != nil {
384 return err
385 }
386
387 gitList := &templates.GitItemPage{
388 Name: name,
389 Ref: ref,
390 GitItemBase: &templates.GitItemCommitPage{
391 Commit: commit,
392 Diff: code.Bytes(),
393 },
394 }
395 templates.WritePageTemplate(w, gitList, r.Context())
396 return nil
397}
398
399func GetLexers(filename string) chroma.Lexer {
400 if filename == "APKBUILD" {
401 return lexers.Get("sh")
402 }
403
404 if strings.HasSuffix(filename, ".qtpl") {
405 return lexers.Get("html")
406 }
407
408 lexer := lexers.Get(filename)
409
410 if lexer == nil {
411 lexer = lexers.Get("txt")
412 }
413 return lexer
414}
415
416func isPublic(r *service.Repository) bool {
417 return r.Public
418}
419
420func orderBy(repos []*service.Repository, order config.OrderBy) []*service.Repository {
421 switch order {
422 case config.AlphabeticalAsc:
423 sort.Slice(repos, func(i, j int) bool {
424 return repos[i].Name < repos[j].Name
425 })
426 case config.AlphabeticalDesc:
427 sort.Slice(repos, func(i, j int) bool {
428 return repos[i].Name > repos[j].Name
429 })
430 case config.LastCommitAsc:
431 sort.Slice(repos, func(i, j int) bool {
432 return repos[i].LastCommit.Commit().Committer.When.Before(repos[j].LastCommit.Commit().Committer.When)
433 })
434 case config.LastCommitDesc:
435 sort.Slice(repos, func(i, j int) bool {
436 return repos[i].LastCommit.Commit().Committer.When.After(repos[j].LastCommit.Commit().Committer.When)
437 })
438 }
439
440 return repos
441}