parent:
966bf149ae7ecdf11b644f19c529f04b0168c580
nmyk <nick@nmyk.io>
2026-02-17T15:52:32-05:00
dance this mess around
diff --git a/.gitignore b/.gitignore
index 5c741111b4a4fe370275ef61f2ca479173da4f45..d40673f79b55b1fd2b391aecc3f03b52a2b0b9c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*.git
vendor/
+.DS_Store
\ No newline at end of file
diff --git a/cmd/bidet/main.go b/cmd/bidet/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..a3da711b81909251c33d6d225ad55e41a0c92679
--- /dev/null
+++ b/cmd/bidet/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "log"
+ "net/http"
+
+ "nmyk.io/bidet/internal/handlers"
+)
+
+const addr = ":8080"
+
+func main() {
+ log.Printf("Serving on %s\n", addr)
+ log.Fatal(http.ListenAndServe(addr, handlers.Routes()))
+}
diff --git a/go.mod b/go.mod
index 0cc96d7da5ecf3428aee037c0a9d5a232c639998..45053c62ff6d84549a834f84ba40827a56d0be76 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
-module bidet
+module nmyk.io/bidet
-go 1.24.0
+go 1.25.0
require github.com/go-git/go-git/v5 v5.16.5
diff --git a/internal/core/git.go b/internal/core/git.go
new file mode 100644
index 0000000000000000000000000000000000000000..46aa7a5b796c20158e13b1ac96220343c850e1b1
--- /dev/null
+++ b/internal/core/git.go
@@ -0,0 +1,136 @@
+package core
+
+import (
+ "errors"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+const DOT_GIT = ".git"
+
+func isBareRepo(e os.DirEntry) bool {
+ return (e.IsDir() && strings.HasSuffix(e.Name(), DOT_GIT) && e.Name() != DOT_GIT)
+}
+
+func List(dir string) ([]string, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ var repos []string
+ for _, e := range entries {
+ if isBareRepo(e) {
+ repos = append(repos, strings.TrimSuffix(e.Name(), DOT_GIT))
+ }
+ }
+ if len(repos) == 0 {
+ return nil, errors.New("No repos found in " + dir)
+ }
+ return repos, nil
+}
+
+type Repo struct {
+ *git.Repository
+}
+
+func Open(repoName string) (*Repo, error) {
+ if _, err := os.Stat(repoName + DOT_GIT); os.IsNotExist(err) {
+ return nil, err
+ }
+ repo, err := git.PlainOpen(repoName + DOT_GIT)
+ if err != nil {
+ return nil, err
+ }
+ return &Repo{repo}, nil
+}
+
+func (repo *Repo) Load(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 Commit struct {
+ *object.Commit
+ Hash plumbing.Hash
+ RefName string
+}
+
+func (repo *Repo) ParseRoute(route string) (*Commit, string) {
+ if route == "" {
+ return nil, ""
+ }
+ candidate := strings.TrimPrefix(route, "/")
+ for {
+ ref, err := repo.resolveRef(candidate)
+ var relPath string
+ if err == nil { // candidate is a tag or a branch name
+ if len(candidate) < len(route) {
+ relPath = strings.TrimPrefix(route[len(candidate):], "/")
+ }
+ refName := ref.Name().Short()
+ commit, _ := repo.resolveCommit(refName)
+ return &Commit{commit, commit.Hash, refName}, relPath
+ }
+ commit, err := repo.resolveCommit(candidate)
+ if err == nil { // candidate is a commit hash
+ if len(candidate) < len(route) {
+ relPath = strings.TrimPrefix(route[len(candidate):], "/")
+ }
+ return &Commit{commit, commit.Hash, ""}, relPath
+ }
+ parent := path.Dir(candidate)
+ if parent == "." || parent == candidate {
+ break
+ }
+ candidate = parent
+ }
+ return nil, ""
+}
+
+func (repo *Repo) resolveCommit(name string) (*object.Commit, error) {
+ hash, err := repo.ResolveRevision(plumbing.Revision(name))
+ if err != nil {
+ return nil, err
+ }
+ return repo.CommitObject(*hash)
+}
+
+func (repo *Repo) resolveRef(name string) (*plumbing.Reference, error) {
+ // first check for branches with the given name
+ branchRef := plumbing.ReferenceName(path.Join("refs", "heads", name))
+ ref, err := repo.Reference(branchRef, true)
+ if err == nil {
+ return ref, nil
+ }
+ // then tags
+ tagRef := plumbing.ReferenceName(path.Join("refs", "tags", name))
+ ref, err = repo.Reference(tagRef, true)
+ if err != nil {
+ return nil, err
+ }
+ obj, err := repo.Object(plumbing.AnyObject, ref.Hash())
+ if err != nil {
+ return nil, err
+ }
+ // if it's an annotated tag, extract the target
+ if tagObj, ok := obj.(*object.Tag); ok {
+ ref = plumbing.NewHashReference(ref.Name(), tagObj.Target)
+ }
+ return ref, nil
+}
diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..30d441a267daf1c079f95ecef36cc6b954f48a71
--- /dev/null
+++ b/internal/handlers/handler.go
@@ -0,0 +1,468 @@
+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)
+}
diff --git a/internal/handlers/static/style.css b/internal/handlers/static/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..d444c08d5f2a27c0a080c6563935313a707a9ad0
--- /dev/null
+++ b/internal/handlers/static/style.css
@@ -0,0 +1,74 @@
+@media (prefers-color-scheme: dark) {
+ :root {
+ --main-text-color: gainsboro;
+ --background-color: black;
+ --alt-text-color: lightskyblue;
+ --header-color: plum;
+ --accent-color: palegreen;
+ --unaccent-color: slategrey;
+ --contrast-color: lightcoral;
+ --selection-bg-color: navy;
+ }
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --main-text-color: black;
+ --background-color: gainsboro;
+ --alt-text-color: mediumblue;
+ --header-color: purple;
+ --accent-color: green;
+ --unaccent-color: slategrey;
+ --contrast-color: firebrick;
+ --selection-bg-color: skyblue;
+ }
+}
+
+body {
+ color: var(--main-text-color);
+ background: var(--background-color);
+ margin: auto;
+ max-width: 80ch;
+ font-size: 14px;
+ padding: 1em;
+ font-family: monospace;
+ word-wrap: break-word;
+}
+
+h1,
+h2,
+h3 {
+ color: var(--header-color);
+}
+
+::selection {
+ background: var(--selection-bg-color);
+}
+
+hr {
+ border-color: var(--header-color);
+}
+
+a {
+ color: var(--alt-text-color);
+ text-decoration: underline;
+}
+
+a:hover {
+ color: var(--alt-text-color);
+ text-decoration: underline;
+}
+
+ul {
+ margin: 0px;
+ padding-left: 20px;
+}
+
+ul {
+ list-style-type: "";
+}
+
+pre {
+ overflow-x: auto;
+ padding-bottom: 10px;
+}
\ No newline at end of file
diff --git a/internal/handlers/templates/base.tmpl b/internal/handlers/templates/base.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..7c11b26b0a6bd1ec08d0e2724bde19e46d1945e6
--- /dev/null
+++ b/internal/handlers/templates/base.tmpl
@@ -0,0 +1,13 @@
+{{define "base"}}
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>{{block "title" .}}Default{{end}}</title>
+ <style>{{ css }}</style>
+</head>
+<body>
+ {{block "content" . }}{{end}}
+</body>
+</html>
+{{end}}
\ No newline at end of file
diff --git a/main.go b/main.go
deleted file mode 100644
index 91f51d533db929d4af3dcf0742a47071f54504f4..0000000000000000000000000000000000000000
--- a/main.go
+++ /dev/null
@@ -1,588 +0,0 @@
-package main
-
-import (
- "bytes"
- "embed"
- "html/template"
- "log"
- "net/http"
- "os"
- "path"
- "slices"
- "strings"
- "time"
-
- 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}/refs/{type}", refs)
- mux.HandleFunc("GET /{name}/tree/{path...}", repoTree)
- mux.HandleFunc("GET /{name}/blob/{path...}", blob)
- mux.HandleFunc("GET /{name}/commits/{path...}", commits)
- mux.HandleFunc("GET /{name}/commit/{hash}", commit)
- 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 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")
- 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
- }
- 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")
- 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
- }
- commit, _ := parse(repo, 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 {
- Repo string
- Hash string
- Author string
- When string
- Message string
- Parents []string
- Patch string
-}
-
-func commit(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
- }
- 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{
- Repo: repoName,
- Hash: commit.Hash.String(),
- Author: fmtAuthor(commit.Author.Name, commit.Author.Email),
- When: commit.Author.When.Format(time.RFC3339),
- Message: commit.Message,
- Parents: parents,
- Patch: patch,
- }
- serve(w, "commit", data)
-}
-
-type Commit struct {
- *object.Commit
- Hash plumbing.Hash
- RefName string
-}
-
-func parse(repo *git.Repository, refAndPath string) (*Commit, string) {
- if refAndPath == "" {
- return nil, ""
- }
- candidate := strings.TrimPrefix(refAndPath, "/")
- for {
- ref, err := resolveRef(repo, candidate)
- var urlPath string
- if err == nil {
- if len(candidate) < len(refAndPath) {
- urlPath = strings.TrimPrefix(refAndPath[len(candidate):], "/")
- }
- refName := ref.Name().Short()
- commit, _ := resolveCommit(repo, refName)
- return &Commit{commit, commit.Hash, refName}, urlPath
- }
- commit, err := resolveCommit(repo, candidate)
- if err == nil {
- if len(candidate) < len(refAndPath) {
- urlPath = strings.TrimPrefix(refAndPath[len(candidate):], "/")
- }
- return &Commit{commit, commit.Hash, ""}, urlPath
- }
- parent := path.Dir(candidate)
- if parent == "." || parent == candidate {
- break
- }
- candidate = parent
- }
- return nil, ""
-}
-
-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
- }
- commit, filePath := parse(repo, 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 := load(repo, 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 := struct {
- Repo string
- RefLabel string
- Base string
- Crumbs []Crumb
- Content string
- }{
- Repo: repoName,
- RefLabel: refLabel,
- Base: 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
- Base string
- Crumbs []Crumb
- Entries []EntryMeta
- ReadmeName string
- Readme string
-}
-
-type EntryMeta struct {
- Name string
- Path string
- IsDir bool
-}
-
-func resolveCommit(repo *git.Repository, name string) (*object.Commit, error) {
- hash, err := repo.ResolveRevision(plumbing.Revision(name))
- if err != nil {
- return nil, err
- }
- return repo.CommitObject(*hash)
-}
-
-func resolveRef(repo *git.Repository, refName string) (*plumbing.Reference, error) {
- branchRef := plumbing.ReferenceName(path.Join("refs", "heads", refName))
- ref, err := repo.Reference(branchRef, true)
- if err == nil {
- return ref, nil
- }
- tagRef := plumbing.ReferenceName(path.Join("refs", "tags", refName))
- ref, err = repo.Reference(tagRef, true)
- if err != nil {
- return nil, err
- }
- // Resolve annotated tag to commit hash
- obj, err := repo.Object(plumbing.AnyObject, ref.Hash())
- if err != nil {
- return nil, err
- }
- if tagObj, ok := obj.(*object.Tag); ok {
- ref = plumbing.NewHashReference(ref.Name(), tagObj.Target)
- }
- return ref, nil
-}
-
-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
- }
- urlPath := r.PathValue("path")
- commit, treePath := parse(repo, 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 == "" {
- repoIndex(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 = load(repo, &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,
- Base: 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 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)
-}
diff --git a/templates/base.tmpl b/templates/base.tmpl
deleted file mode 100644
index 3ef06f834cf217b6b14894f0a1b92cfab00f610e..0000000000000000000000000000000000000000
--- a/templates/base.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-{{define "base"}}
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="UTF-8">
- <title>{{block "title" .}}Default{{end}}</title>
- <link rel="stylesheet" href="https://nmyk.io/style.css">
- <link rel="stylesheet" href="https://nmyk.io/doc.css">
-</head>
-<body>
- {{block "content" .}}{{end}}
-</body>
-</html>
-{{end}}
\ No newline at end of file
diff --git a/templates/blob.tmpl b/internal/handlers/templates/blob.tmpl
rename from templates/blob.tmpl
rename to internal/handlers/templates/blob.tmpl
index 90f900a4ddb0dede3d5cf6345f6c845aacff0fed..4df727dda838c9627ec21bb3f878c47065f51de5 100644
--- a/templates/blob.tmpl
+++ b/internal/handlers/templates/blob.tmpl
@@ -1,6 +1,6 @@
-{{define "title"}}{{.Repo}}/{{.Base}}{{end}}
+{{define "title"}}{{.Repo}}/{{.Path}}{{end}}
{{define "content"}}
-<h1>{{if eq .Base ""}}{{.Repo}}{{else}}<a href="/{{.Repo}}/tree/{{.RefLabel}}">{{.Repo}}</a> /{{end}}
+<h1>{{if eq .Path ""}}{{.Repo}}{{else}}<a href="/{{.Repo}}/tree/{{.RefLabel}}">{{.Repo}}</a> /{{end}}
{{range $i, $c := .Crumbs}}
{{if $i}} / {{end}}
{{if $c.IsLast}}
diff --git a/templates/commit.tmpl b/internal/handlers/templates/commit.tmpl
rename from templates/commit.tmpl
rename to internal/handlers/templates/commit.tmpl
index 20f010e4b9e89a2a8971baa1897855df4f986fd9..adeeaaad1ee84f4b8f03d142da830cfe20f066cd 100644
--- a/templates/commit.tmpl
+++ b/internal/handlers/templates/commit.tmpl
@@ -7,8 +7,7 @@ <p>parents: {{ range .Parents }}<br><a href="/{{$.Repo}}/commit/{{.}}">{{.}}</a> {{end}}</p>
<p>{{.Author}}</p>
<p>{{.When}}</p>
<pre>{{.Message}}</pre>
- <hr>
- {{if ne "" .Patch}}<pre>{{.Patch}}</pre>{{else}}<i>(initial commit)</i>{{end}}
+ {{if ne "" .Patch}}<hr><pre>{{.Patch}}</pre>{{end}}
{{end}}
{{define "commit"}}
diff --git a/templates/commits.tmpl b/internal/handlers/templates/commits.tmpl
rename from templates/commits.tmpl
rename to internal/handlers/templates/commits.tmpl
diff --git a/templates/refs.tmpl b/internal/handlers/templates/refs.tmpl
rename from templates/refs.tmpl
rename to internal/handlers/templates/refs.tmpl
diff --git a/templates/repo.tmpl b/internal/handlers/templates/repo.tmpl
rename from templates/repo.tmpl
rename to internal/handlers/templates/repo.tmpl
index 15ab581d855d323b3223801b752d209fb5332cbb..c741b6262e9cfa1d6a57731c297413a2bec68df6 100644
--- a/templates/repo.tmpl
+++ b/internal/handlers/templates/repo.tmpl
@@ -1,4 +1,4 @@
-{{define "title"}}{{.Repo}}/{{.Base}}{{end}}
+{{define "title"}}{{.Repo}}/{{.Path}}{{end}}
{{define "content"}}
<h1>{{.Repo}}</h1>
<p>@ {{.Ref}}</p>
diff --git a/templates/repos.tmpl b/internal/handlers/templates/repos.tmpl
rename from templates/repos.tmpl
rename to internal/handlers/templates/repos.tmpl
diff --git a/templates/tree.tmpl b/internal/handlers/templates/tree.tmpl
rename from templates/tree.tmpl
rename to internal/handlers/templates/tree.tmpl
index 16bad38dd41285f8e2c50864e98ee0e2e67732d4..8ae54adaba821181839da5f0a682f2caa5f9fc0c 100644
--- a/templates/tree.tmpl
+++ b/internal/handlers/templates/tree.tmpl
@@ -1,6 +1,6 @@
-{{define "title"}}{{.Repo}}/{{.Base}}{{end}}
+{{define "title"}}{{.Repo}}/{{.Path}}{{end}}
{{define "content"}}
-<h1>{{if ne .Base ""}}<a href="/{{.Repo}}/tree/{{.RefLabel}}">{{.Repo}}</a> /
+<h1>{{if ne .Path ""}}<a href="/{{.Repo}}/tree/{{.RefLabel}}">{{.Repo}}</a> /
{{else}}
<a href="/{{.Repo}}/tree/{{.HeadRef}}">{{.Repo}}</a>
{{end}}
@@ -13,7 +13,7 @@ <a href="/{{$c.URL}}">{{$c.Name}}</a>
{{end}}
{{end}}</h1>
<p>@ {{.RefLabel}}</p>
-{{if eq .Base ""}}
+{{if eq .Path ""}}
<p><a href="/">index</a> | <a href="/{{.Repo}}/refs">refs</a>{{if ne .Ref ""}} | <a href="/{{.Repo}}/commits/{{.Ref}}">commits</a>{{end}}</p>
{{end}}
<ul>