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