cerrado @ v0.0.9

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