cerrado @ f4ea96c52928eedd11b3f809cd9a481eda214482

  1package git
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"log/slog"
  9	"net/http"
 10	"os"
 11	"path/filepath"
 12	"sort"
 13	"strings"
 14
 15	"git.gabrielgio.me/cerrado/pkg/config"
 16	"git.gabrielgio.me/cerrado/pkg/ext"
 17	"git.gabrielgio.me/cerrado/pkg/service"
 18	"git.gabrielgio.me/cerrado/pkg/u"
 19	"git.gabrielgio.me/cerrado/templates"
 20	"github.com/alecthomas/chroma/v2"
 21	"github.com/alecthomas/chroma/v2/formatters/html"
 22	"github.com/alecthomas/chroma/v2/lexers"
 23	"github.com/alecthomas/chroma/v2/styles"
 24	"github.com/go-git/go-git/v5/plumbing/object"
 25	"github.com/gomarkdown/markdown"
 26	markdownhtml "github.com/gomarkdown/markdown/html"
 27	"github.com/gomarkdown/markdown/parser"
 28)
 29
 30type (
 31	GitHandler struct {
 32		gitService *service.GitService
 33		config     configurationRepository
 34	}
 35
 36	configurationRepository interface {
 37		GetRootReadme() string
 38		GetSyntaxHighlight() string
 39		GetOrderBy() config.OrderBy
 40	}
 41)
 42
 43func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler {
 44	return &GitHandler{
 45		gitService: gitService,
 46		config:     confRepo,
 47	}
 48}
 49
 50func (g *GitHandler) List(w http.ResponseWriter, r *http.Request) error {
 51	// this is the only handler that needs to handle authentication itself.
 52	// everything else relay on name path parameter
 53	logged := ext.IsLoggedIn(r.Context())
 54
 55	repos, err := g.gitService.ListRepositories()
 56	if err != nil {
 57		return err
 58	}
 59
 60	if !logged {
 61		repos = u.Filter(repos, isPublic)
 62	}
 63
 64	f, err := os.Open(g.config.GetRootReadme())
 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: orderBy(repos, g.config.GetOrderBy()),
 86		About:         bs,
 87	}
 88	templates.WritePageTemplate(w, gitList, r.Context())
 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, r)
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, r.Context())
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 errors.Is(err, object.ErrFileNotFound) {
164		templates.WritePageTemplate(w, &templates.GitItemPage{
165			Name: name,
166			Ref:  ref.Name().Short(),
167			GitItemBase: &templates.GitItemAboutPage{
168				About: []byte("About file not configured properly"),
169			},
170		}, r.Context())
171		return nil
172	}
173	if err != nil {
174		return err
175	}
176
177	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
178	p := parser.NewWithExtensions(extensions)
179	doc := p.Parse(file)
180
181	htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank
182	opts := markdownhtml.RendererOptions{Flags: htmlFlag}
183	renderer := markdownhtml.NewRenderer(opts)
184
185	bs := markdown.Render(doc, renderer)
186
187	gitList := &templates.GitItemPage{
188		Name: name,
189		Ref:  ref.Name().Short(),
190		GitItemBase: &templates.GitItemAboutPage{
191			About: bs,
192		},
193	}
194	templates.WritePageTemplate(w, gitList, r.Context())
195	return nil
196}
197
198func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error {
199	ext.SetHTML(w)
200	name := r.PathValue("name")
201
202	tags, err := g.gitService.ListTags(name)
203	if err != nil {
204		return err
205	}
206
207	branches, err := g.gitService.ListBranches(name)
208	if err != nil {
209		return err
210	}
211
212	ref, err := g.gitService.GetHead(name)
213	if err != nil {
214		return err
215	}
216
217	gitList := &templates.GitItemPage{
218		Name: name,
219		Ref:  ref.Name().Short(),
220		GitItemBase: &templates.GitItemRefsPage{
221			Tags:     tags,
222			Branches: branches,
223		},
224	}
225	templates.WritePageTemplate(w, gitList, r.Context())
226	return nil
227}
228
229func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error {
230	ext.SetHTML(w)
231	name := r.PathValue("name")
232	ref := r.PathValue("ref")
233	rest := r.PathValue("rest")
234	paths := []string{}
235
236	// this is avoid Split from generating a len 1 array with empty string
237	if rest != "" {
238		paths = strings.Split(rest, "/")
239	}
240
241	tree, err := g.gitService.GetTree(name, ref, rest)
242	if err != nil {
243		return err
244	}
245
246	gitList := &templates.GitItemPage{
247		Name: name,
248		Ref:  ref,
249		GitItemBase: &templates.GitItemTreePage{
250			Path: paths,
251			Tree: tree,
252		},
253	}
254	templates.WritePageTemplate(w, gitList, r.Context())
255	return nil
256}
257
258func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error {
259	ext.SetHTML(w)
260	name := r.PathValue("name")
261	ref := r.PathValue("ref")
262	rest := r.PathValue("rest")
263	paths := []string{}
264
265	// this is avoid Split from generating a len 1 array with empty string
266	if rest != "" {
267		paths = strings.Split(rest, "/")
268	}
269
270	isBin, err := g.gitService.IsBinary(name, ref, rest)
271	if err != nil {
272		return err
273	}
274
275	// if it is binary no need to over all the chroma process
276	if isBin {
277		gitList := &templates.GitItemPage{
278			Name: name,
279			Ref:  ref,
280			GitItemBase: &templates.GitItemBlobPage{
281				Path:    paths,
282				Content: []byte("Binary file"),
283			},
284		}
285		templates.WritePageTemplate(w, gitList, r.Context())
286		return nil
287	}
288
289	file, err := g.gitService.GetFileContent(name, ref, rest)
290	if err != nil {
291		return err
292	}
293
294	filename := filepath.Base(rest)
295	lexer := GetLexers(filename)
296	style := styles.Get(g.config.GetSyntaxHighlight())
297
298	formatter := html.New(
299		html.WithLineNumbers(true),
300		html.WithLinkableLineNumbers(true, "L"),
301	)
302
303	iterator, err := lexer.Tokenise(nil, string(file))
304	if err != nil {
305		return err
306	}
307
308	var code bytes.Buffer
309	err = formatter.Format(&code, style, iterator)
310	if err != nil {
311		return err
312	}
313
314	gitList := &templates.GitItemPage{
315		Name: name,
316		Ref:  ref,
317		GitItemBase: &templates.GitItemBlobPage{
318			Path:    paths,
319			Content: code.Bytes(),
320		},
321	}
322	templates.WritePageTemplate(w, gitList, r.Context())
323	return nil
324}
325
326func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error {
327	ext.SetHTML(w)
328	name := r.PathValue("name")
329	ref := r.PathValue("ref")
330	from := r.URL.Query().Get("from")
331
332	commits, next, err := g.gitService.ListCommits(name, ref, from, 100)
333	if err != nil {
334		return err
335	}
336
337	gitList := &templates.GitItemPage{
338		Name: name,
339		Ref:  ref,
340		GitItemBase: &templates.GitItemLogPage{
341			Commits: commits,
342			Next:    next,
343		},
344	}
345	templates.WritePageTemplate(w, gitList, r.Context())
346	return nil
347}
348
349func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error {
350	ext.SetHTML(w)
351	name := r.PathValue("name")
352	ref := r.PathValue("ref")
353
354	commit, err := g.gitService.LastCommit(name, ref)
355	if err != nil {
356		return err
357	}
358
359	diff, err := g.gitService.Diff(name, ref)
360	if err != nil {
361		return err
362	}
363
364	lexer := lexers.Get("diff")
365	style := styles.Get(g.config.GetSyntaxHighlight())
366
367	formatter := html.New(
368		html.WithLineNumbers(true),
369		html.WithLinkableLineNumbers(true, "L"),
370	)
371
372	iterator, err := lexer.Tokenise(nil, diff)
373	if err != nil {
374		return err
375	}
376
377	var code bytes.Buffer
378	err = formatter.Format(&code, style, iterator)
379	if err != nil {
380		return err
381	}
382
383	gitList := &templates.GitItemPage{
384		Name: name,
385		Ref:  ref,
386		GitItemBase: &templates.GitItemCommitPage{
387			Commit: commit,
388			Diff:   code.Bytes(),
389		},
390	}
391	templates.WritePageTemplate(w, gitList, r.Context())
392	return nil
393}
394
395func GetLexers(filename string) chroma.Lexer {
396	if filename == "APKBUILD" {
397		return lexers.Get("sh")
398	}
399
400	if strings.HasSuffix(filename, ".qtpl") {
401		return lexers.Get("html")
402	}
403
404	lexer := lexers.Get(filename)
405
406	if lexer == nil {
407		lexer = lexers.Get("txt")
408	}
409	return lexer
410}
411
412func isPublic(r *service.Repository) bool {
413	return r.Public
414}
415
416func orderBy(repos []*service.Repository, order config.OrderBy) []*service.Repository {
417	switch order {
418	case config.AlphabeticalAsc:
419		sort.Slice(repos, func(i, j int) bool {
420			return repos[i].Name < repos[j].Name
421		})
422	case config.AlphabeticalDesc:
423		sort.Slice(repos, func(i, j int) bool {
424			return repos[i].Name > repos[j].Name
425		})
426	case config.LastCommitAsc:
427		sort.Slice(repos, func(i, j int) bool {
428			return repos[i].LastCommit.Commit().Committer.When.Before(repos[j].LastCommit.Commit().Committer.When)
429		})
430	case config.LastCommitDesc:
431		sort.Slice(repos, func(i, j int) bool {
432			return repos[i].LastCommit.Commit().Committer.When.After(repos[j].LastCommit.Commit().Committer.When)
433		})
434	}
435
436	return repos
437}