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