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
237 tree, err := g.gitService.GetTree(name, ref, rest)
238 if err != nil {
239 return err
240 }
241
242 gitList := &templates.GitItemPage{
243 Name: name,
244 Ref: ref,
245 GitItemBase: &templates.GitItemTreePage{
246 CurrentPath: rest,
247 Tree: tree,
248 },
249 }
250 templates.WritePageTemplate(w, gitList)
251 return nil
252}
253
254func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error {
255 ext.SetHTML(w)
256 name := r.PathValue("name")
257 ref := r.PathValue("ref")
258 rest := r.PathValue("rest")
259
260 isBin, err := g.gitService.IsBinary(name, ref, rest)
261 if err != nil {
262 return err
263 }
264
265 // if it is binary no need to over all the chroma process
266 if isBin {
267 gitList := &templates.GitItemPage{
268 Name: name,
269 Ref: ref,
270 GitItemBase: &templates.GitItemBlobPage{
271 File: rest,
272 Content: []byte("Binary file"),
273 },
274 }
275 templates.WritePageTemplate(w, gitList)
276 return nil
277 }
278
279 file, err := g.gitService.GetFileContent(name, ref, rest)
280 if err != nil {
281 return err
282 }
283
284 filename := filepath.Base(rest)
285 lexer := GetLexers(filename)
286 style := styles.Get("xcode")
287 formatter := html.New(
288 html.WithLineNumbers(true),
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 File: rest,
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 gitList := &templates.GitItemPage{
346 Name: name,
347 Ref: ref,
348 GitItemBase: &templates.GitItemCommitPage{
349 Commit: commit,
350 },
351 }
352 templates.WritePageTemplate(w, gitList)
353 return nil
354}
355
356func GetLexers(filename string) chroma.Lexer {
357 if filename == "APKBUILD" {
358 return lexers.Get("sh")
359 }
360
361 lexer := lexers.Get(filename)
362
363 if lexer == nil {
364 lexer = lexers.Get("txt")
365 }
366 return lexer
367}