cerrado @ cb6060a60d71ce1be1591bb10f499916155160de

  1package git
  2
  3import (
  4	"bytes"
  5	"compress/gzip"
  6	"errors"
  7	"fmt"
  8	"io"
  9	"log/slog"
 10	"net/http"
 11	"os"
 12	"path/filepath"
 13	"sort"
 14	"strings"
 15
 16	"git.gabrielgio.me/cerrado/pkg/config"
 17	"git.gabrielgio.me/cerrado/pkg/ext"
 18	"git.gabrielgio.me/cerrado/pkg/service"
 19	"git.gabrielgio.me/cerrado/pkg/u"
 20	"git.gabrielgio.me/cerrado/templates"
 21	"github.com/alecthomas/chroma/v2"
 22	"github.com/alecthomas/chroma/v2/formatters/html"
 23	"github.com/alecthomas/chroma/v2/lexers"
 24	"github.com/alecthomas/chroma/v2/styles"
 25	"github.com/go-git/go-git/v5/plumbing/object"
 26	"github.com/gomarkdown/markdown"
 27	markdownhtml "github.com/gomarkdown/markdown/html"
 28	"github.com/gomarkdown/markdown/parser"
 29)
 30
 31type (
 32	GitHandler struct {
 33		gitService *service.GitService
 34		config     configurationRepository
 35	}
 36
 37	configurationRepository interface {
 38		GetRootReadme() string
 39		GetSyntaxHighlight() string
 40		GetOrderBy() config.OrderBy
 41	}
 42)
 43
 44func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler {
 45	return &GitHandler{
 46		gitService: gitService,
 47		config:     confRepo,
 48	}
 49}
 50
 51func (g *GitHandler) List(w http.ResponseWriter, r *ext.Request) error {
 52	// this is the only handler that needs to handle authentication itself.
 53	// everything else relay on name path parameter
 54	logged := ext.IsLoggedIn(r.Context())
 55
 56	repos, err := g.gitService.ListRepositories()
 57	if err != nil {
 58		return err
 59	}
 60
 61	if !logged {
 62		repos = u.Filter(repos, isPublic)
 63	}
 64
 65	f, err := os.Open(g.config.GetRootReadme())
 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: orderBy(repos, g.config.GetOrderBy()),
 87		About:         bs,
 88	}
 89	templates.WritePageTemplate(w, gitList, r.Context())
 90	return nil
 91}
 92
 93func (g *GitHandler) Archive(w http.ResponseWriter, r *ext.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, r)
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) Multiplex(w http.ResponseWriter, r *ext.Request) error {
120	path := r.PathValue("rest")
121	name := r.PathValue("name")
122
123	if r.URL.RawQuery == "service=git-receive-pack" {
124		ext.BadRequest(w, r, "no pushing allowed")
125		return nil
126	}
127
128	if path == "info/refs" && r.URL.RawQuery == "service=git-upload-pack" && r.Method == "GET" {
129		w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
130
131		err := g.gitService.WriteInfoRefs(r.Context(), name, w)
132		if err != nil {
133			slog.Error("Error WriteInfoRefs", "error", err)
134		}
135	} else if path == "git-upload-pack" && r.Method == "POST" {
136		w.Header().Set("content-type", "application/x-git-upload-pack-result")
137		w.Header().Set("Connection", "Keep-Alive")
138		w.Header().Set("Transfer-Encoding", "chunked")
139		w.WriteHeader(http.StatusOK)
140
141		reader := r.Body
142
143		if r.Header.Get("Content-Encoding") == "gzip" {
144			var err error
145			reader, err = gzip.NewReader(r.Body)
146			if err != nil {
147				return err
148			}
149			defer reader.Close()
150		}
151
152		err := g.gitService.WriteUploadPack(r.Context(), name, reader, w)
153		if err != nil {
154			slog.Error("Error WriteUploadPack", "error", err)
155		}
156	} else if r.Method == "GET" {
157		return g.Summary(w, r)
158	}
159
160	return nil
161}
162
163func (g *GitHandler) Summary(w http.ResponseWriter, r *ext.Request) error {
164	ext.SetHTML(w)
165	name := r.PathValue("name")
166	ref, err := g.gitService.GetHead(name)
167	if err != nil {
168		return err
169	}
170
171	tags, err := g.gitService.ListTags(name)
172	if err != nil {
173		return err
174	}
175
176	branches, err := g.gitService.ListBranches(name)
177	if err != nil {
178		return err
179	}
180
181	commits, _, err := g.gitService.ListCommits(name, "", "", 10)
182	if err != nil {
183		return err
184	}
185
186	if len(tags) > 3 {
187		tags = tags[:3]
188	}
189
190	gitList := &templates.GitItemPage{
191		Name: name,
192		Ref:  ref.Name().Short(),
193		GitItemBase: &templates.GitItemSummaryPage{
194			Tags:     tags,
195			Branches: branches,
196			Commits:  commits,
197		},
198	}
199	templates.WritePageTemplate(w, gitList, r.Context())
200	return nil
201}
202
203func (g *GitHandler) About(w http.ResponseWriter, r *ext.Request) error {
204	ext.SetHTML(w)
205	name := r.PathValue("name")
206	ref, err := g.gitService.GetHead(name)
207	if err != nil {
208		return err
209	}
210
211	file, err := g.gitService.GetAbout(name)
212	if errors.Is(err, object.ErrFileNotFound) {
213		templates.WritePageTemplate(w, &templates.GitItemPage{
214			Name: name,
215			Ref:  ref.Name().Short(),
216			GitItemBase: &templates.GitItemAboutPage{
217				About: []byte("About file not configured properly"),
218			},
219		}, r.Context())
220		return nil
221	}
222	if err != nil {
223		return err
224	}
225
226	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
227	p := parser.NewWithExtensions(extensions)
228	doc := p.Parse(file)
229
230	htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
231	opts := markdownhtml.RendererOptions{Flags: htmlFlag}
232	renderer := markdownhtml.NewRenderer(opts)
233
234	bs := markdown.Render(doc, renderer)
235
236	gitList := &templates.GitItemPage{
237		Name: name,
238		Ref:  ref.Name().Short(),
239		GitItemBase: &templates.GitItemAboutPage{
240			About: bs,
241		},
242	}
243	templates.WritePageTemplate(w, gitList, r.Context())
244	return nil
245}
246
247func (g *GitHandler) Refs(w http.ResponseWriter, r *ext.Request) error {
248	ext.SetHTML(w)
249	name := r.PathValue("name")
250
251	tags, err := g.gitService.ListTags(name)
252	if err != nil {
253		return err
254	}
255
256	branches, err := g.gitService.ListBranches(name)
257	if err != nil {
258		return err
259	}
260
261	ref, err := g.gitService.GetHead(name)
262	if err != nil {
263		return err
264	}
265
266	gitList := &templates.GitItemPage{
267		Name: name,
268		Ref:  ref.Name().Short(),
269		GitItemBase: &templates.GitItemRefsPage{
270			Tags:     tags,
271			Branches: branches,
272		},
273	}
274	templates.WritePageTemplate(w, gitList, r.Context())
275	return nil
276}
277
278func (g *GitHandler) Tree(w http.ResponseWriter, r *ext.Request) error {
279	ext.SetHTML(w)
280	name := r.PathValue("name")
281	ref := r.PathValue("ref")
282	rest := r.PathValue("rest")
283	paths := []string{}
284
285	// this is avoid Split from generating a len 1 array with empty string
286	if rest != "" {
287		paths = strings.Split(rest, "/")
288	}
289
290	tree, err := g.gitService.GetTree(name, ref, rest)
291	if err != nil {
292		return err
293	}
294
295	gitList := &templates.GitItemPage{
296		Name: name,
297		Ref:  ref,
298		GitItemBase: &templates.GitItemTreePage{
299			Path: paths,
300			Tree: tree,
301		},
302	}
303	templates.WritePageTemplate(w, gitList, r.Context())
304	return nil
305}
306
307func (g *GitHandler) Blob(w http.ResponseWriter, r *ext.Request) error {
308	ext.SetHTML(w)
309	name := r.PathValue("name")
310	ref := r.PathValue("ref")
311	rest := r.PathValue("rest")
312	paths := []string{}
313
314	// this is avoid Split from generating a len 1 array with empty string
315	if rest != "" {
316		paths = strings.Split(rest, "/")
317	}
318
319	isBin, err := g.gitService.IsBinary(name, ref, rest)
320	if err != nil {
321		return err
322	}
323
324	// if it is binary no need to over all the chroma process
325	if isBin {
326		gitList := &templates.GitItemPage{
327			Name: name,
328			Ref:  ref,
329			GitItemBase: &templates.GitItemBlobPage{
330				Path:    paths,
331				Content: []byte("Binary file"),
332			},
333		}
334		templates.WritePageTemplate(w, gitList, r.Context())
335		return nil
336	}
337
338	file, err := g.gitService.GetFileContent(name, ref, rest)
339	if err != nil {
340		return err
341	}
342
343	filename := filepath.Base(rest)
344	lexer := GetLexers(filename)
345	style := styles.Get(g.config.GetSyntaxHighlight())
346
347	formatter := html.New(
348		html.WithLineNumbers(true),
349		html.WithLinkableLineNumbers(true, "L"),
350	)
351
352	iterator, err := lexer.Tokenise(nil, string(file))
353	if err != nil {
354		return err
355	}
356
357	var code bytes.Buffer
358	err = formatter.Format(&code, style, iterator)
359	if err != nil {
360		return err
361	}
362
363	gitList := &templates.GitItemPage{
364		Name: name,
365		Ref:  ref,
366		GitItemBase: &templates.GitItemBlobPage{
367			Path:    paths,
368			Content: code.Bytes(),
369		},
370	}
371	templates.WritePageTemplate(w, gitList, r.Context())
372	return nil
373}
374
375func (g *GitHandler) Log(w http.ResponseWriter, r *ext.Request) error {
376	ext.SetHTML(w)
377	name := r.PathValue("name")
378	ref := r.PathValue("ref")
379	from := r.URL.Query().Get("from")
380
381	commits, next, err := g.gitService.ListCommits(name, ref, from, 100)
382	if err != nil {
383		return err
384	}
385
386	gitList := &templates.GitItemPage{
387		Name: name,
388		Ref:  ref,
389		GitItemBase: &templates.GitItemLogPage{
390			Commits: commits,
391			Next:    next,
392		},
393	}
394	templates.WritePageTemplate(w, gitList, r.Context())
395	return nil
396}
397
398func (g *GitHandler) Ref(w http.ResponseWriter, r *ext.Request) error {
399	ext.SetHTML(w)
400	name := r.PathValue("name")
401	ref := r.PathValue("ref")
402
403	commit, tag, err := g.gitService.GetTag(ref, name)
404	if err != nil {
405		return err
406	}
407
408	gitList := &templates.GitItemPage{
409		Name: name,
410		Ref:  ref,
411		GitItemBase: &templates.GitItemRefPage{
412			Commit:    commit,
413			Reference: tag,
414		},
415	}
416	templates.WritePageTemplate(w, gitList, r.Context())
417	return nil
418}
419
420func (g *GitHandler) Commit(w http.ResponseWriter, r *ext.Request) error {
421	ext.SetHTML(w)
422	name := r.PathValue("name")
423	ref := r.PathValue("ref")
424
425	commit, err := g.gitService.LastCommit(name, ref)
426	if err != nil {
427		return err
428	}
429
430	diff, err := g.gitService.Diff(name, ref)
431	if err != nil {
432		return err
433	}
434
435	lexer := lexers.Get("diff")
436	style := styles.Get(g.config.GetSyntaxHighlight())
437
438	formatter := html.New(
439		html.WithLineNumbers(true),
440		html.WithLinkableLineNumbers(true, "L"),
441	)
442
443	iterator, err := lexer.Tokenise(nil, diff)
444	if err != nil {
445		return err
446	}
447
448	var code bytes.Buffer
449	err = formatter.Format(&code, style, iterator)
450	if err != nil {
451		return err
452	}
453
454	gitList := &templates.GitItemPage{
455		Name: name,
456		Ref:  ref,
457		GitItemBase: &templates.GitItemCommitPage{
458			Commit: commit,
459			Diff:   code.Bytes(),
460		},
461	}
462	templates.WritePageTemplate(w, gitList, r.Context())
463	return nil
464}
465
466func GetLexers(filename string) chroma.Lexer {
467	if filename == "APKBUILD" {
468		return lexers.Get("sh")
469	}
470
471	if strings.HasSuffix(filename, ".qtpl") {
472		return lexers.Get("html")
473	}
474
475	lexer := lexers.Get(filename)
476
477	if lexer == nil {
478		lexer = lexers.Get("txt")
479	}
480	return lexer
481}
482
483func isPublic(r *service.Repository) bool {
484	return r.Public
485}
486
487func orderBy(repos []*service.Repository, order config.OrderBy) []*service.Repository {
488	switch order {
489	case config.AlphabeticalAsc:
490		sort.Slice(repos, func(i, j int) bool {
491			return repos[i].Name < repos[j].Name
492		})
493	case config.AlphabeticalDesc:
494		sort.Slice(repos, func(i, j int) bool {
495			return repos[i].Name > repos[j].Name
496		})
497	case config.LastCommitAsc:
498		sort.Slice(repos, func(i, j int) bool {
499			return repos[i].LastCommit.Commit().Committer.When.Before(repos[j].LastCommit.Commit().Committer.When)
500		})
501	case config.LastCommitDesc:
502		sort.Slice(repos, func(i, j int) bool {
503			return repos[i].LastCommit.Commit().Committer.When.After(repos[j].LastCommit.Commit().Committer.When)
504		})
505	}
506
507	return repos
508}