bidet / main.go

@ 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)
}