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