@ d75a654d61ba5dd1809a0bfb15f0eac2daa7ee4a | history
package main
import (
"bytes"
"embed"
"html/template"
"log"
"net/http"
"os"
"path"
"strings"
git "github.com/go-git/go-git/v5"
"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 dotGit = ".git"
func main() {
http.HandleFunc("/", router)
log.Printf("Serving on %s\n", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
type Repo struct {
*git.Repository
Name string
}
func router(w http.ResponseWriter, r *http.Request) {
path := strings.Trim(r.URL.Path, "/")
if path == "" {
listRepos(w)
return
}
parts := strings.Split(path, "/")
repoName := parts[0]
repoPath := repoName + dotGit
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
gitRepo, err := git.PlainOpen(repoPath)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
repo := Repo{gitRepo, repoName}
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
}
if len(parts) == 1 {
renderTree(w, &repo, "", tree)
return
}
subPath := strings.Join(parts[1:], "/")
handlePath(w, &repo, subPath, tree)
}
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) {
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(), dotGit) &&
e.Name() != dotGit)
}
for _, e := range entries {
if isBareRepo(e) {
repos = append(repos, strings.TrimSuffix(e.Name(), dotGit))
}
}
serve(w, "repos", repos)
}
func handlePath(w http.ResponseWriter, repo *Repo, subPath string, rootTree *object.Tree) {
entry, err := rootTree.FindEntry(subPath)
if err != nil {
http.NotFound(w, nil)
return
}
if entry.Mode == filemode.Dir {
tree, err := repo.TreeObject(entry.Hash)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
renderTree(w, repo, subPath, tree)
return
}
file, err := repo.BlobObject(entry.Hash)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
reader, err := file.Reader()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer reader.Close()
content := make([]byte, file.Size)
reader.Read(content)
data := struct {
Repo string
Dir string
Path string
Content string
}{
Repo: repo.Name,
Dir: path.Dir(subPath),
Path: subPath,
Content: string(content),
}
serve(w, "file", data)
}
func renderTree(w http.ResponseWriter, repo *Repo, subPath string, tree *object.Tree) {
type Entry struct {
Name string
Path string
IsDir bool
}
var entries []Entry
for _, e := range tree.Entries {
fullPath := path.Join(subPath, e.Name)
entries = append(entries, Entry{
Name: e.Name,
Path: path.Join("/", repo.Name, fullPath),
IsDir: e.Mode == filemode.Dir,
})
}
data := struct {
Repo string
Dir string
Base string
Entries []Entry
}{
Repo: repo.Name,
Dir: path.Dir(subPath),
Base: subPath,
Entries: entries,
}
serve(w, "tree", data)
}