package git import ( "bytes" "errors" "fmt" "io" "log/slog" "net/http" "os" "path/filepath" "strings" "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/pkg/service" "git.gabrielgio.me/cerrado/pkg/u" "git.gabrielgio.me/cerrado/templates" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/go-git/go-git/v5/plumbing/object" "github.com/gomarkdown/markdown" markdownhtml "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) type ( GitHandler struct { gitService *service.GitService config configurationRepository } configurationRepository interface { GetRootReadme() string GetSyntaxHighlight() string } ) func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler { return &GitHandler{ gitService: gitService, config: confRepo, } } func (g *GitHandler) List(w http.ResponseWriter, r *http.Request) error { // this is the only handler that needs to handle authentication itself. // everything else relay on name path parameter logged := ext.IsLoggedIn(r.Context()) repos, err := g.gitService.ListRepositories() if err != nil { return err } if !logged { repos = u.Filter(repos, isPublic) } f, err := os.Open(g.config.GetRootReadme()) if err != nil { return err } bs, err := io.ReadAll(f) if err != nil { return err } extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock p := parser.NewWithExtensions(extensions) doc := p.Parse(bs) htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank opts := markdownhtml.RendererOptions{Flags: htmlFlag} renderer := markdownhtml.NewRenderer(opts) bs = markdown.Render(doc, renderer) gitList := &templates.GitListPage{ Respositories: repos, About: bs, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error { ext.SetGZip(w) name := r.PathValue("name") file := r.PathValue("file") ref := strings.TrimSuffix(file, ".tar.gz") // TODO: remove it once we can support more than gzip if !strings.HasSuffix(file, ".tar.gz") { ext.NotFound(w, r) return nil } filename := fmt.Sprintf("%s-%s.tar.gz", name, ref) ext.SetFileName(w, filename) prefix := fmt.Sprintf("%s-%s", name, ref) err := g.gitService.WriteTarGZip(w, name, ref, prefix) if err != nil { // once we start writing to the body we can't report error anymore // so we are only left with printing the error. slog.Error("Error generating tar gzip file", "error", err) } return nil } func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref, err := g.gitService.GetHead(name) if err != nil { return err } tags, err := g.gitService.ListTags(name) if err != nil { return err } branches, err := g.gitService.ListBranches(name) if err != nil { return err } commits, err := g.gitService.ListCommits(name, "", 10) if err != nil { return err } gitList := &templates.GitItemPage{ Name: name, Ref: ref.Name().Short(), GitItemBase: &templates.GitItemSummaryPage{ Tags: tags, Branches: branches, Commits: commits, }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) About(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref, err := g.gitService.GetHead(name) if err != nil { return err } file, err := g.gitService.GetAbout(name) if errors.Is(err, object.ErrFileNotFound) { templates.WritePageTemplate(w, &templates.GitItemPage{ Name: name, Ref: ref.Name().Short(), GitItemBase: &templates.GitItemAboutPage{ About: []byte("About file not configured properly"), }, }, r.Context()) return nil } if err != nil { return err } extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock p := parser.NewWithExtensions(extensions) doc := p.Parse(file) htmlFlag := markdownhtml.CommonFlags | markdownhtml.HrefTargetBlank opts := markdownhtml.RendererOptions{Flags: htmlFlag} renderer := markdownhtml.NewRenderer(opts) bs := markdown.Render(doc, renderer) gitList := &templates.GitItemPage{ Name: name, Ref: ref.Name().Short(), GitItemBase: &templates.GitItemAboutPage{ About: bs, }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) Refs(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") tags, err := g.gitService.ListTags(name) if err != nil { return err } branches, err := g.gitService.ListBranches(name) if err != nil { return err } ref, err := g.gitService.GetHead(name) if err != nil { return err } gitList := &templates.GitItemPage{ Name: name, Ref: ref.Name().Short(), GitItemBase: &templates.GitItemRefsPage{ Tags: tags, Branches: branches, }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") rest := r.PathValue("rest") paths := []string{} // this is avoid Split from generating a len 1 array with empty string if rest != "" { paths = strings.Split(rest, "/") } tree, err := g.gitService.GetTree(name, ref, rest) if err != nil { return err } gitList := &templates.GitItemPage{ Name: name, Ref: ref, GitItemBase: &templates.GitItemTreePage{ Path: paths, Tree: tree, }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") rest := r.PathValue("rest") paths := []string{} // this is avoid Split from generating a len 1 array with empty string if rest != "" { paths = strings.Split(rest, "/") } isBin, err := g.gitService.IsBinary(name, ref, rest) if err != nil { return err } // if it is binary no need to over all the chroma process if isBin { gitList := &templates.GitItemPage{ Name: name, Ref: ref, GitItemBase: &templates.GitItemBlobPage{ Path: paths, Content: []byte("Binary file"), }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } file, err := g.gitService.GetFileContent(name, ref, rest) if err != nil { return err } filename := filepath.Base(rest) lexer := GetLexers(filename) style := styles.Get(g.config.GetSyntaxHighlight()) formatter := html.New( html.WithLineNumbers(true), html.WithLinkableLineNumbers(true, "L"), ) iterator, err := lexer.Tokenise(nil, string(file)) if err != nil { return err } var code bytes.Buffer err = formatter.Format(&code, style, iterator) if err != nil { return err } gitList := &templates.GitItemPage{ Name: name, Ref: ref, GitItemBase: &templates.GitItemBlobPage{ Path: paths, Content: code.Bytes(), }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) Log(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") commits, err := g.gitService.ListCommits(name, ref, 1000) if err != nil { return err } gitList := &templates.GitItemPage{ Name: name, Ref: ref, GitItemBase: &templates.GitItemLogPage{ Commits: commits, }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func (g *GitHandler) Commit(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") ref := r.PathValue("ref") commit, err := g.gitService.LastCommit(name, ref) if err != nil { return err } diff, err := g.gitService.Diff(name, ref) if err != nil { return err } gitList := &templates.GitItemPage{ Name: name, Ref: ref, GitItemBase: &templates.GitItemCommitPage{ Commit: commit, Diff: diff, }, } templates.WritePageTemplate(w, gitList, r.Context()) return nil } func GetLexers(filename string) chroma.Lexer { if filename == "APKBUILD" { return lexers.Get("sh") } if strings.HasSuffix(filename, ".qtpl") { return lexers.Get("html") } lexer := lexers.Get(filename) if lexer == nil { lexer = lexers.Get("txt") } return lexer } func isPublic(r *service.Repository) bool { return r.Public }