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