cerrado @ 2c0347566f99afec2e3963d74f4fc970e6187217

  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 *http.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 *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, 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 *http.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			reader, err := gzip.NewReader(r.Body)
145			if err != nil {
146				return err
147			}
148			defer reader.Close()
149		}
150
151		err := g.gitService.WriteUploadPack(r.Context(), name, reader, w)
152		if err != nil {
153			slog.Error("Error WriteUploadPack", "error", err)
154		}
155	} else if r.Method == "GET" {
156		return g.Summary(w, r)
157	}
158
159	return nil
160}
161
162func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error {
163	ext.SetHTML(w)
164	name := r.PathValue("name")
165	ref, err := g.gitService.GetHead(name)
166	if err != nil {
167		return err
168	}
169
170	tags, err := g.gitService.ListTags(name)
171	if err != nil {
172		return err
173	}
174
175	branches, err := g.gitService.ListBranches(name)
176	if err != nil {
177		return err
178	}
179
180	commits, _, err := g.gitService.ListCommits(name, "", "", 10)
181	if err != nil {
182		return err
183	}
184
185	if len(tags) > 3 {
186		tags = tags[:3]
187	}
188
189	gitList := &templates.GitItemPage{
190		Name: name,
191		Ref:  ref.Name().Short(),
192		GitItemBase: &templates.GitItemSummaryPage{
193			Tags:     tags,
194			Branches: branches,
195			Commits:  commits,
196		},
197	}
198	templates.WritePageTemplate(w, gitList, r.Context())
199	return nil
200}
201
202func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error {
203	ext.SetHTML(w)
204	name := r.PathValue("name")
205	ref, err := g.gitService.GetHead(name)
206	if err != nil {
207		return err
208	}
209
210	file, err := g.gitService.GetAbout(name)
211	if errors.Is(err, object.ErrFileNotFound) {
212		templates.WritePageTemplate(w, &templates.GitItemPage{
213			Name: name,
214			Ref:  ref.Name().Short(),
215			GitItemBase: &templates.GitItemAboutPage{
216				About: []byte("About file not configured properly"),
217			},
218		}, r.Context())
219		return nil
220	}
221	if err != nil {
222		return err
223	}
224
225	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
226	p := parser.NewWithExtensions(extensions)
227	doc := p.Parse(file)
228
229	htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
230	opts := markdownhtml.RendererOptions{Flags: htmlFlag}
231	renderer := markdownhtml.NewRenderer(opts)
232
233	bs := markdown.Render(doc, renderer)
234
235	gitList := &templates.GitItemPage{
236		Name: name,
237		Ref:  ref.Name().Short(),
238		GitItemBase: &templates.GitItemAboutPage{
239			About: bs,
240		},
241	}
242	templates.WritePageTemplate(w, gitList, r.Context())
243	return nil
244}
245
246func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error {
247	ext.SetHTML(w)
248	name := r.PathValue("name")
249
250	tags, err := g.gitService.ListTags(name)
251	if err != nil {
252		return err
253	}
254
255	branches, err := g.gitService.ListBranches(name)
256	if err != nil {
257		return err
258	}
259
260	ref, err := g.gitService.GetHead(name)
261	if err != nil {
262		return err
263	}
264
265	gitList := &templates.GitItemPage{
266		Name: name,
267		Ref:  ref.Name().Short(),
268		GitItemBase: &templates.GitItemRefsPage{
269			Tags:     tags,
270			Branches: branches,
271		},
272	}
273	templates.WritePageTemplate(w, gitList, r.Context())
274	return nil
275}
276
277func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error {
278	ext.SetHTML(w)
279	name := r.PathValue("name")
280	ref := r.PathValue("ref")
281	rest := r.PathValue("rest")
282	paths := []string{}
283
284	// this is avoid Split from generating a len 1 array with empty string
285	if rest != "" {
286		paths = strings.Split(rest, "/")
287	}
288
289	tree, err := g.gitService.GetTree(name, ref, rest)
290	if err != nil {
291		return err
292	}
293
294	gitList := &templates.GitItemPage{
295		Name: name,
296		Ref:  ref,
297		GitItemBase: &templates.GitItemTreePage{
298			Path: paths,
299			Tree: tree,
300		},
301	}
302	templates.WritePageTemplate(w, gitList, r.Context())
303	return nil
304}
305
306func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error {
307	ext.SetHTML(w)
308	name := r.PathValue("name")
309	ref := r.PathValue("ref")
310	rest := r.PathValue("rest")
311	paths := []string{}
312
313	// this is avoid Split from generating a len 1 array with empty string
314	if rest != "" {
315		paths = strings.Split(rest, "/")
316	}
317
318	isBin, err := g.gitService.IsBinary(name, ref, rest)
319	if err != nil {
320		return err
321	}
322
323	// if it is binary no need to over all the chroma process
324	if isBin {
325		gitList := &templates.GitItemPage{
326			Name: name,
327			Ref:  ref,
328			GitItemBase: &templates.GitItemBlobPage{
329				Path:    paths,
330				Content: []byte("Binary file"),
331			},
332		}
333		templates.WritePageTemplate(w, gitList, r.Context())
334		return nil
335	}
336
337	file, err := g.gitService.GetFileContent(name, ref, rest)
338	if err != nil {
339		return err
340	}
341
342	filename := filepath.Base(rest)
343	lexer := GetLexers(filename)
344	style := styles.Get(g.config.GetSyntaxHighlight())
345
346	formatter := html.New(
347		html.WithLineNumbers(true),
348		html.WithLinkableLineNumbers(true, "L"),
349	)
350
351	iterator, err := lexer.Tokenise(nil, string(file))
352	if err != nil {
353		return err
354	}
355
356	var code bytes.Buffer
357	err = formatter.Format(&code, style, iterator)
358	if err != nil {
359		return err
360	}
361
362	gitList := &templates.GitItemPage{
363		Name: name,
364		Ref:  ref,
365		GitItemBase: &templates.GitItemBlobPage{
366			Path:    paths,
367			Content: code.Bytes(),
368		},
369	}
370	templates.WritePageTemplate(w, gitList, r.Context())
371	return nil
372}
373
374func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error {
375	ext.SetHTML(w)
376	name := r.PathValue("name")
377	ref := r.PathValue("ref")
378	from := r.URL.Query().Get("from")
379
380	commits, next, err := g.gitService.ListCommits(name, ref, from, 100)
381	if err != nil {
382		return err
383	}
384
385	gitList := &templates.GitItemPage{
386		Name: name,
387		Ref:  ref,
388		GitItemBase: &templates.GitItemLogPage{
389			Commits: commits,
390			Next:    next,
391		},
392	}
393	templates.WritePageTemplate(w, gitList, r.Context())
394	return nil
395}
396
397func (g *GitHandler) Ref(w http.ResponseWriter, r *http.Request) error {
398	ext.SetHTML(w)
399	name := r.PathValue("name")
400	ref := r.PathValue("ref")
401
402	commit, tag, err := g.gitService.GetTag(ref, name)
403	if err != nil {
404		return err
405	}
406
407	gitList := &templates.GitItemPage{
408		Name: name,
409		Ref:  ref,
410		GitItemBase: &templates.GitItemRefPage{
411			Commit:    commit,
412			Reference: tag,
413		},
414	}
415	templates.WritePageTemplate(w, gitList, r.Context())
416	return nil
417}
418
419func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error {
420	ext.SetHTML(w)
421	name := r.PathValue("name")
422	ref := r.PathValue("ref")
423
424	commit, err := g.gitService.LastCommit(name, ref)
425	if err != nil {
426		return err
427	}
428
429	diff, err := g.gitService.Diff(name, ref)
430	if err != nil {
431		return err
432	}
433
434	lexer := lexers.Get("diff")
435	style := styles.Get(g.config.GetSyntaxHighlight())
436
437	formatter := html.New(
438		html.WithLineNumbers(true),
439		html.WithLinkableLineNumbers(true, "L"),
440	)
441
442	iterator, err := lexer.Tokenise(nil, diff)
443	if err != nil {
444		return err
445	}
446
447	var code bytes.Buffer
448	err = formatter.Format(&code, style, iterator)
449	if err != nil {
450		return err
451	}
452
453	gitList := &templates.GitItemPage{
454		Name: name,
455		Ref:  ref,
456		GitItemBase: &templates.GitItemCommitPage{
457			Commit: commit,
458			Diff:   code.Bytes(),
459		},
460	}
461	templates.WritePageTemplate(w, gitList, r.Context())
462	return nil
463}
464
465func GetLexers(filename string) chroma.Lexer {
466	if filename == "APKBUILD" {
467		return lexers.Get("sh")
468	}
469
470	if strings.HasSuffix(filename, ".qtpl") {
471		return lexers.Get("html")
472	}
473
474	lexer := lexers.Get(filename)
475
476	if lexer == nil {
477		lexer = lexers.Get("txt")
478	}
479	return lexer
480}
481
482func isPublic(r *service.Repository) bool {
483	return r.Public
484}
485
486func orderBy(repos []*service.Repository, order config.OrderBy) []*service.Repository {
487	switch order {
488	case config.AlphabeticalAsc:
489		sort.Slice(repos, func(i, j int) bool {
490			return repos[i].Name < repos[j].Name
491		})
492	case config.AlphabeticalDesc:
493		sort.Slice(repos, func(i, j int) bool {
494			return repos[i].Name > repos[j].Name
495		})
496	case config.LastCommitAsc:
497		sort.Slice(repos, func(i, j int) bool {
498			return repos[i].LastCommit.Commit().Committer.When.Before(repos[j].LastCommit.Commit().Committer.When)
499		})
500	case config.LastCommitDesc:
501		sort.Slice(repos, func(i, j int) bool {
502			return repos[i].LastCommit.Commit().Committer.When.After(repos[j].LastCommit.Commit().Committer.When)
503		})
504	}
505
506	return repos
507}