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