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