@ 585be7e7e1255e44cef779b4eeab47ee22ff35f7 | history
package main
import (
"bytes"
"embed"
"html/template"
"log"
"net/http"
"os"
"path"
"slices"
"strings"
git "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"
)
//go:embed templates/*.tmpl
var templates embed.FS
const addr = ":8080"
const DOT_GIT = ".git"
func main() {
log.Printf("Serving on %s\n", addr)
log.Fatal(http.ListenAndServe(addr, router()))
}
type Repo struct {
*git.Repository
Name string
}
func router() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /", listRepos)
mux.HandleFunc("GET /{name}", repoIndex)
mux.HandleFunc("GET /{name}/refs", refs)
mux.HandleFunc("GET /{name}/tree/{ref}", repoTree)
mux.HandleFunc("GET /{name}/tree/{ref}/{rest...}", repoTree)
mux.HandleFunc("GET /{name}/blob/{ref}/{rest...}", blob)
return mux
}
func serve(w http.ResponseWriter, name string, data any) {
tmpl := template.Must(template.ParseFS(
templates,
"templates/base.tmpl",
"templates/"+name+".tmpl",
))
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
http.Error(w, err.Error(), 500)
return
}
w.WriteHeader(http.StatusOK)
buf.WriteTo(w)
}
func listRepos(w http.ResponseWriter, _ *http.Request) {
entries, err := os.ReadDir(".")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var repos []string
isBareRepo := func(e os.DirEntry) bool {
return (e.IsDir() &&
strings.HasSuffix(e.Name(), DOT_GIT) &&
e.Name() != DOT_GIT)
}
for _, e := range entries {
if isBareRepo(e) {
repos = append(repos, strings.TrimSuffix(e.Name(), DOT_GIT))
}
}
serve(w, "repos", repos)
}
func load(repo *git.Repository, entry *object.TreeEntry) (string, error) {
file, err := repo.BlobObject(entry.Hash)
if err != nil {
return "", err
}
reader, err := file.Reader()
if err != nil {
return "", err
}
defer reader.Close()
content := make([]byte, file.Size)
reader.Read(content)
return string(content), nil
}
type BranchMeta struct {
Name string
}
type RefsData struct {
Repo string
Branches []BranchMeta
}
func refs(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
if _, err := os.Stat(repoName + DOT_GIT); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
repo, err := git.PlainOpen(repoName + DOT_GIT)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
iter, err := repo.Branches()
if err != nil {
http.Error(w, err.Error(), 500)
}
var branches []BranchMeta
err = iter.ForEach(func(ref *plumbing.Reference) error {
branches = append(branches, BranchMeta{ref.Name().Short()})
return nil
})
if err != nil {
http.Error(w, err.Error(), 500)
}
data := RefsData{
Repo: repoName,
Branches: branches,
}
serve(w, "refs", data)
}
func blob(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
if _, err := os.Stat(repoName + DOT_GIT); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
repo, err := git.PlainOpen(repoName + DOT_GIT)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
refName := r.PathValue("ref")
branch := path.Join("refs", "heads", refName)
ref, err := repo.Reference(plumbing.ReferenceName(branch), true)
if err != nil {
http.NotFound(w, r)
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
}
filePath := r.PathValue("rest")
entry, err := tree.FindEntry(filePath)
if err != nil {
http.NotFound(w, nil)
return
}
content, err := load(repo, entry)
if err != nil {
http.Error(w, err.Error(), 500)
}
data := struct {
Repo string
Ref string
Base string
Crumbs []Crumb
Content string
}{
Repo: repoName,
Ref: refName,
Base: filePath,
Crumbs: breadcrumbs(repoName, refName, filePath),
Content: content,
}
serve(w, "blob", data)
}
type TreeData struct {
Repo string
Ref string
HeadRef string
Base string
Crumbs []Crumb
Entries []EntryMeta
ReadmeName string
Readme string
}
type EntryMeta struct {
Name string
Path string
IsDir bool
}
func repoTree(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
if _, err := os.Stat(repoName + DOT_GIT); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
repo, err := git.PlainOpen(repoName + DOT_GIT)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
refName := r.PathValue("ref")
head, err := repo.Head()
if err != nil {
http.Error(w, "Cannot resolve HEAD state", 500)
}
headRef := head.Name().Short()
treePath := r.PathValue("rest")
if refName == headRef && treePath == "" {
repoIndex(w, r)
return
}
branch := path.Join("refs", "heads", refName)
ref, err := repo.Reference(plumbing.ReferenceName(branch), true)
if err != nil {
http.NotFound(w, r)
return
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
http.Error(w, err.Error(), 500)
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 = load(repo, &e)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
entries = append(entries, em)
}
data := TreeData{
Repo: repoName,
Ref: refName,
HeadRef: headRef,
Base: treePath,
Crumbs: breadcrumbs(repoName, refName, treePath),
Entries: entries,
ReadmeName: readmeName,
Readme: readme,
}
serve(w, "tree", data)
}
type Crumb struct {
Name string
URL string
IsLast bool
}
func breadcrumbs(repoName string, refName 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", refName, cur),
IsLast: last,
})
last = false
next := path.Dir(cur)
if next == cur {
break
}
cur = next
}
slices.Reverse(crumbs)
return crumbs
}
func repoIndex(w http.ResponseWriter, r *http.Request) {
repoName := r.PathValue("name")
if _, err := os.Stat(repoName + DOT_GIT); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
repo, err := git.PlainOpen(repoName + DOT_GIT)
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 = load(repo, &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)
}