cerrado @ master

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