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