@ e2c0ba88ca54836cd58fce3f4052860786b92c33 | history
package handlers
import (
"bytes"
"embed"
"html/template"
"net/http"
"path"
"slices"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"nmyk.io/bidet/internal/core"
)
func Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /", ListRepos)
mux.HandleFunc("GET /{name}", Repo)
mux.HandleFunc("GET /{name}/refs", Refs)
mux.HandleFunc("GET /{name}/refs/{type}", Refs)
mux.HandleFunc("GET /{name}/tree/{path...}", Tree)
mux.HandleFunc("GET /{name}/blob/{path...}", Blob)
mux.HandleFunc("GET /{name}/commits/{path...}", Commits)
mux.HandleFunc("GET /{name}/commit/{hash}", Commit)
return mux
}
//go:embed templates/*.tmpl
var Templates embed.FS
//go:embed static/style.css
var styleCSS []byte
func Serve(w http.ResponseWriter, tmplName string, data any) {
tmpl := template.Must(template.New("").
Funcs(template.FuncMap{
"css": func() template.CSS {
return template.CSS(styleCSS)
},
}).
ParseFS(
Templates,
"templates/base.tmpl",
"templates/"+tmplName+".tmpl",
),
)
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, tmplName, data); err != nil {
http.Error(w, err.Error(), 500)
return
}
w.WriteHeader(http.StatusOK)
buf.WriteTo(w)
}
func ListRepos(w http.ResponseWriter, _ *http.Request) {
repos, err := core.List(".")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
Serve(w, "repos", repos)
}
type BranchMeta struct {
Name string
}
type TagMeta struct {
Name string
}
type RefsData struct {
Repo string
Type string
Branches []BranchMeta
Tags []TagMeta
}
func Refs(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
branchIter, err := repo.Branches()
if err != nil {
http.Error(w, err.Error(), 500)
}
var branches []BranchMeta
err = branchIter.ForEach(func(ref *plumbing.Reference) error {
branches = append(branches, BranchMeta{ref.Name().Short()})
return nil
})
if err != nil {
http.Error(w, err.Error(), 500)
}
tagIter, err := repo.Tags()
if err != nil {
http.Error(w, err.Error(), 500)
}
var tags []TagMeta
err = tagIter.ForEach(func(ref *plumbing.Reference) error {
tags = append(tags, TagMeta{ref.Name().Short()})
return nil
})
if err != nil {
http.Error(w, err.Error(), 500)
}
refType := r.PathValue("type")
if !slices.Contains([]string{"branches", "tags"}, refType) {
refType = "branches"
}
data := RefsData{
Repo: repoName,
Type: refType,
Branches: branches,
Tags: tags,
}
Serve(w, "refs", data)
}
type CommitMeta struct {
Hash string
Author string
When string
Message string
}
type CommitsData struct {
Repo string
Ref string
Commits []CommitMeta
}
func fmtAuthor(name string, email string) string {
return name + " <" + email + ">"
}
func Commits(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
commit, _ := repo.ParseRoute(r.PathValue("path"))
iter, err := repo.Log(&git.LogOptions{
From: commit.Hash,
})
if err != nil {
http.Error(w, err.Error(), 500)
}
commits := []CommitMeta{}
err = iter.ForEach(func(c *object.Commit) error {
commits = append(commits, CommitMeta{
Hash: c.Hash.String(),
Author: fmtAuthor(c.Author.Name, c.Author.Email),
When: c.Author.When.Format(time.RFC3339),
Message: c.Message,
})
return nil
})
if err != nil {
http.Error(w, err.Error(), 500)
}
data := CommitsData{
Repo: repoName,
Ref: commit.RefName,
Commits: commits,
}
Serve(w, "commits", data)
}
type CommitData struct {
CommitMeta
Repo string
Parents []string
Patch string
}
func Commit(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
hashStr := r.PathValue("hash")
hash := plumbing.NewHash(hashStr)
commit, err := repo.CommitObject(hash)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var (
parents []string
patch string
i int
)
iter := commit.Parents()
err = iter.ForEach(func(c *object.Commit) error {
if i == 0 {
p, err := c.Patch(commit)
if err != nil {
return err
}
patch = p.String()
}
parents = append(parents, string(c.Hash.String()))
return nil
})
if err != nil {
http.Error(w, err.Error(), 500)
}
data := CommitData{
CommitMeta: CommitMeta{
Hash: commit.Hash.String(),
Author: fmtAuthor(commit.Author.Name, commit.Author.Email),
When: commit.Author.When.Format(time.RFC3339),
Message: commit.Message,
},
Repo: repoName,
Parents: parents,
Patch: patch,
}
Serve(w, "commit", data)
}
type BlobData struct {
Repo string
RefLabel string
Path string
Crumbs []Crumb
Content string
}
func Blob(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
commit, filePath := repo.ParseRoute(r.PathValue("path"))
tree, err := commit.Tree()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
entry, err := tree.FindEntry(filePath)
if err != nil {
http.NotFound(w, nil)
return
}
content, err := repo.Load(entry)
if err != nil {
http.Error(w, err.Error(), 500)
}
var refLabel string
if commit.RefName != "" {
refLabel = commit.RefName
} else {
refLabel = commit.Hash.String()
}
data := BlobData{
Repo: repoName,
RefLabel: refLabel,
Path: filePath,
Crumbs: breadcrumbs(repoName, refLabel, filePath),
Content: content,
}
Serve(w, "blob", data)
}
type TreeData struct {
Repo string
Ref string
RefLabel string
Commit string
HeadRef string
Path string
Crumbs []Crumb
Entries []EntryMeta
ReadmeName string
Readme string
}
type EntryMeta struct {
Name string
Path string
IsDir bool
}
func Tree(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
urlPath := r.PathValue("path")
commit, treePath := repo.ParseRoute(urlPath)
head, err := repo.Head()
if err != nil {
http.Error(w, "Cannot resolve HEAD state", 500)
}
headRef := head.Name().Short()
if commit.RefName == headRef && treePath == "" {
Repo(w, r)
return
}
rootTree, err := commit.Tree()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var tree *object.Tree
if treePath != "" {
treeEntry, err := rootTree.FindEntry(treePath)
if err != nil {
http.NotFound(w, r)
return
}
tree, err = repo.TreeObject(treeEntry.Hash)
if err != nil {
http.Error(w, err.Error(), 500)
}
} else {
tree = rootTree
}
var entries []EntryMeta
if treePath != "" {
entries = []EntryMeta{
{
Name: "..",
Path: path.Dir(treePath),
IsDir: true,
},
}
}
var readme, readmeName string
for _, e := range tree.Entries {
fullPath := path.Join(treePath, e.Name)
em := EntryMeta{
Name: e.Name,
Path: fullPath,
IsDir: e.Mode == filemode.Dir,
}
if strings.HasPrefix(strings.ToLower(e.Name), "readme") {
readmeName = e.Name
var err error
readme, err = repo.Load(&e)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
entries = append(entries, em)
}
var refLabel string
if commit.RefName != "" {
refLabel = commit.RefName
} else {
refLabel = commit.Hash.String()
}
data := TreeData{
Repo: repoName,
RefLabel: refLabel,
Ref: commit.RefName,
HeadRef: headRef,
Path: treePath,
Crumbs: breadcrumbs(repoName, refLabel, treePath),
Entries: entries,
ReadmeName: readmeName,
Readme: readme,
}
Serve(w, "tree", data)
}
type Crumb struct {
Name string
URL string
IsLast bool
}
func breadcrumbs(repoName string, refLabel string, p string) []Crumb {
var crumbs []Crumb
last := true
cur := p
for {
base := path.Base(cur)
if base == "." || base == "/" {
break
}
crumbs = append(crumbs, Crumb{
Name: base,
URL: path.Join(repoName, "tree", refLabel, cur),
IsLast: last,
})
last = false
next := path.Dir(cur)
if next == cur {
break
}
cur = next
}
slices.Reverse(crumbs)
return crumbs
}
func Repo(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
repo, err := core.Open(repoName)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
ref, err := repo.Head()
if err != nil {
http.Error(w, "Cannot resolve HEAD", 500)
return
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
tree, err := commit.Tree()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
refName := ref.Name().Short()
var entries []EntryMeta
var readme, readmeName string
for _, e := range tree.Entries {
em := EntryMeta{
Name: e.Name,
Path: e.Name,
IsDir: e.Mode == filemode.Dir,
}
if strings.HasPrefix(strings.ToLower(e.Name), "readme") {
readmeName = e.Name
var err error
readme, err = repo.Load(&e)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
entries = append(entries, em)
}
data := TreeData{
Repo: repoName,
Ref: refName,
Entries: entries,
ReadmeName: readmeName,
Readme: readme,
}
Serve(w, "repo", data)
}