package handlers import ( "errors" "fmt" "html" "html/template" "log/slog" "net/http" "os" "path/filepath" "sort" "strings" "time" "olexsmir.xyz/mugit/internal/git" "olexsmir.xyz/mugit/internal/markdown" ) type Meta struct { Title string Description string Host string IsEmpty bool GoMod bool SSHEnabled bool } type RepoBase struct { Ref string Desc string } type PageData[T any] struct { Meta Meta RepoName string // empty for non-repo pages, needed for _head.html to compile P T } func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) { repos, err := h.listPublicRepos() if err != nil { h.write500(w, err) return } h.templ(w, "index", h.pageData(nil, repos)) } type RepoIndex struct { Desc string SSHUser string IsEmpty bool Readme template.HTML Ref string Commits []*git.Commit IsMirror bool MirrorURL string MirrorLastSync time.Time MirrorLastChecked time.Time } func (h *handlers) repoIndexHandler(w http.ResponseWriter, r *http.Request) { repo, err := h.openPublicRepo(r.PathValue("name"), "") if err != nil { h.write404(w, r.URL.Path, err) return } desc, err := repo.Description() if err != nil { h.write500(w, err) return } p := RepoIndex{ Desc: desc, IsEmpty: repo.IsEmpty(), SSHUser: h.c.SSH.User, } if isMirror, merr := repo.IsMirror(); isMirror && merr == nil { p.IsMirror = true p.MirrorURL, _ = repo.RemoteURL() p.MirrorLastSync, _ = repo.LastSync() p.MirrorLastChecked, _ = repo.LastChecked() } if p.IsEmpty { h.templ(w, "repo_index", h.pageData(repo, p)) return } p.Ref, err = repo.DefaultBranch() if err != nil { h.write500(w, err) return } p.Readme, err = h.renderReadme(repo, p.Ref, "") if err != nil { h.write500(w, err) return } p.Commits, err = repo.Commits("") if err != nil { h.write500(w, err) return } if len(p.Commits) >= 3 { p.Commits = p.Commits[:3:3] } h.templ(w, "repo_index", h.pageData(repo, p)) } type RepoTree struct { Desc string Ref string Tree []git.NiceTree Breadcrumbs []Breadcrumb ParentPath string DotDot string Readme template.HTML } func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := h.parseRef(r.PathValue("ref")) treePath := r.PathValue("rest") repo, err := h.openPublicRepo(name, ref) if err != nil { h.write404(w, r.URL.Path, err) return } desc, err := repo.Description() if err != nil { h.write500(w, err) return } tree, err := repo.FileTree(r.Context(), treePath) if err != nil { h.write500(w, err) return } readme, err := h.renderReadme(repo, ref, treePath) if err != nil { h.write500(w, err) return } h.templ(w, "repo_tree", h.pageData(repo, RepoTree{ Desc: desc, Ref: ref, Tree: tree, ParentPath: treePath, DotDot: filepath.Dir(treePath), Breadcrumbs: Breadcrumbs(treePath), Readme: readme, })) } type RepoFile struct { Ref string Desc string Lines []string LastCommit *git.Commit Breadcrumbs []Breadcrumb Path string IsImage bool IsBinary bool Mime string Size int64 } func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := h.parseRef(r.PathValue("ref")) treePath := r.PathValue("rest") repo, err := h.openPublicRepo(name, ref) if err != nil { h.write404(w, r.URL.Path, err) return } fc, err := repo.FileContent(treePath) if err != nil { if errors.Is(err, git.ErrFileNotFound) { h.write404(w, r.URL.Path, err) return } h.write500(w, err) return } p := RepoFile{ Ref: ref, Path: treePath, IsImage: fc.IsImage, IsBinary: fc.IsBinary, Mime: fc.Mime, Size: fc.Size, } p.LastCommit, err = repo.LastFileCommit(r.Context(), treePath) if err != nil { h.write500(w, err) return } p.Desc, err = repo.Description() if err != nil { h.write500(w, err) return } p.Breadcrumbs = Breadcrumbs(treePath) if !fc.IsImage && !fc.IsBinary { p.Lines = strings.Split(strings.TrimRight(fc.String(), "\n"), "\n") } h.templ(w, "repo_file", h.pageData(repo, p)) } func (h *handlers) rawFileContentsHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := h.parseRef(r.PathValue("ref")) treePath := r.PathValue("rest") repo, err := h.openPublicRepo(name, ref) if err != nil { w.WriteHeader(http.StatusNotFound) slog.Info("404", "err", err) return } fc, err := repo.FileContent(treePath) if err != nil { if errors.Is(err, git.ErrFileNotFound) { w.WriteHeader(http.StatusNotFound) slog.Info("404", "err", err) return } w.WriteHeader(http.StatusInternalServerError) slog.Info("500", "err", err) return } w.Header().Set("Content-Type", fc.Mime) w.WriteHeader(http.StatusOK) _, _ = w.Write(fc.Content) } type RepoLog struct { Desc string Commits []*git.Commit Ref string NextAfter string } func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := h.parseRef(r.PathValue("ref")) after := r.URL.Query().Get("after") repo, err := h.openPublicRepo(name, ref) if err != nil { h.write404(w, r.URL.Path, err) return } desc, err := repo.Description() if err != nil { h.write500(w, err) return } commits, err := repo.Commits(after) if err != nil { h.write500(w, err) return } // if we got full page of commits, we probably have more. // NOTE: this has an edge case, when last page is len(git.CommitsPage), "load more" would be shown nextAfter := "" if len(commits) == git.CommitsPage && len(commits) > 0 { nextAfter = commits[len(commits)-1].HashShort } h.templ(w, "repo_log", h.pageData(repo, RepoLog{ Desc: desc, Ref: ref, Commits: commits, NextAfter: nextAfter, })) } type RepoCommit struct { Diff *git.NiceDiff Ref string Desc string } func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref := h.parseRef(r.PathValue("ref")) repo, err := h.openPublicRepo(name, ref) if err != nil { h.write404(w, r.URL.Path, err) return } diff, err := h.getDiff(repo, ref) if err != nil { h.write500(w, err) return } desc, err := repo.Description() if err != nil { h.write500(w, err) return } h.templ(w, "repo_commit", h.pageData(repo, RepoCommit{ Desc: desc, Ref: ref, Diff: diff, })) } type RepoCompare struct { Desc string Ref string Compare *git.Compare } func (h *handlers) compareHandler(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") ref1 := h.parseRef(r.PathValue("ref1")) ref2 := h.parseRef(r.PathValue("ref2")) repo, err := h.openPublicRepo(name, ref2) if err != nil { h.write404(w, r.URL.Path, err) return } desc, err := repo.Description() if err != nil { h.write500(w, err) return } compare, err := repo.Compare(ref1, ref2) if err != nil { h.write404(w, r.URL.Path, err) return } h.templ(w, "repo_compare", h.pageData(repo, RepoCompare{ Desc: desc, Ref: ref2, Compare: compare, })) } type RepoRefs struct { Desc string Ref string Branches []*git.Branch Tags []*git.TagReference } func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) { repo, err := h.openPublicRepo(r.PathValue("name"), "") if err != nil { h.write404(w, r.URL.Path, err) return } desc, err := repo.Description() if err != nil { h.write500(w, err) return } master, err := repo.DefaultBranch() if err != nil { h.write500(w, err) return } branches, err := repo.Branches() if err != nil { h.write500(w, err) return } // repo should have at least one branch, tags are *optional* tags, _ := repo.Tags() h.templ(w, "repo_refs", h.pageData(repo, RepoRefs{ Desc: desc, Ref: master, Tags: tags, Branches: branches, })) } type repoList struct { Name string Desc string LastCommit time.Time } func (h *handlers) listPublicRepos() ([]repoList, error) { if v, found := h.repoListCache.Get("repo_list"); found { return v, nil } dirs, err := os.ReadDir(h.c.Repo.Dir) if err != nil { return nil, err } var repos []repoList var errs []error for _, dir := range dirs { if !dir.IsDir() { continue } name := dir.Name() repo, err := h.openPublicRepo(name, "") if err != nil { // if it's not git repo, just ignore it continue } desc, err := repo.Description() if err != nil { errs = append(errs, err) continue } lastCommit, err := repo.LastCommit() if err != nil { errs = append(errs, err) continue } repos = append(repos, repoList{ Name: repo.Name(), Desc: desc, LastCommit: lastCommit.Committed, }) } sort.Slice(repos, func(i, j int) bool { return repos[j].LastCommit.Before(repos[i].LastCommit) }) h.repoListCache.Set("repo_list", repos) return repos, errors.Join(errs...) } func (h handlers) getDiff(r *git.Repo, ref string) (*git.NiceDiff, error) { cacheKey := fmt.Sprintf("%s:%s", r.Name(), ref) if v, found := h.diffCache.Get(cacheKey); found { return v, nil } diff, err := r.Diff() if err != nil { return nil, err } h.diffCache.Set(cacheKey, diff) return diff, nil } func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) { name := r.Name() cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath) if v, found := h.readmeCache.Get(cacheKey); found { return v, nil } var readmeContents template.HTML for _, readme := range h.c.Repo.Readmes { fullPath := filepath.Join(treePath, readme) fc, ferr := r.FileContent(fullPath) if ferr != nil { continue } if fc.IsBinary && fc.IsImage { continue } ext := filepath.Ext(readme) content := fc.String() if len(content) > 0 { switch ext { case ".md", ".markdown", ".mkd": readme, err := markdown.Render(name, ref, fullPath, content) if err != nil { return "", err } return template.HTML(readme), nil default: readmeContents = template.HTML(fmt.Sprintf( `
%s
`, html.EscapeString(content))) } break } } h.readmeCache.Set(cacheKey, readmeContents) return readmeContents, nil } func (h handlers) pageData(repo *git.Repo, p any) PageData[any] { var name string var gomod, empty bool if repo != nil { gomod = repo.IsGoMod() empty = repo.IsEmpty() name = repo.Name() } return PageData[any]{ P: p, RepoName: name, Meta: Meta{ Title: h.c.Meta.Title, Description: h.c.Meta.Description, Host: h.c.Meta.Host, GoMod: gomod, SSHEnabled: h.c.SSH.Enable, IsEmpty: empty, }, } } type Breadcrumb struct { Name string Path string IsLast bool } func Breadcrumbs(path string) []Breadcrumb { if path == "" { return nil } parts := strings.Split(path, "/") crumbs := make([]Breadcrumb, len(parts)) for i, part := range parts { crumbs[i] = Breadcrumb{ Name: part, Path: strings.Join(parts[:i+1], "/"), IsLast: i == len(parts)-1, } } return crumbs }