parent:
105da3591641754954b7b227790d434fad7b3352
nmyk <nick@nmyk.io>
2026-02-16T17:14:58-05:00
add commit history
diff --git a/main.go b/main.go
index d0625a493efbdfe4acbcd61d40408adf8d9c2357..91f51d533db929d4af3dcf0742a47071f54504f4 100644
--- a/main.go
+++ b/main.go
@@ -10,6 +10,7 @@ "os"
"path"
"slices"
"strings"
+ "time"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
@@ -41,6 +42,8 @@ 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
}
@@ -158,19 +161,150 @@ }
serve(w, "refs", data)
}
-func parse(repo *git.Repository, refAndPath string) (refName string, urlPath string) {
+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 "", ""
+ return nil, ""
}
candidate := strings.TrimPrefix(refAndPath, "/")
for {
- ref, err := resolve(repo, candidate)
+ 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()
- return
+ 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 {
@@ -178,7 +312,7 @@ break
}
candidate = parent
}
- return "", ""
+ return nil, ""
}
func blob(w http.ResponseWriter, r *http.Request) {
@@ -192,17 +326,7 @@ if err != nil {
http.Error(w, err.Error(), 500)
return
}
- refName, filePath := parse(repo, r.PathValue("path"))
- ref, err := resolve(repo, refName)
- if err != nil {
- http.NotFound(w, r)
- return
- }
- commit, err := repo.CommitObject(ref.Hash())
- 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)
@@ -217,18 +341,24 @@ 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
- Ref string
- Base string
- Crumbs []Crumb
- Content string
+ Repo string
+ RefLabel string
+ Base string
+ Crumbs []Crumb
+ Content string
}{
- Repo: repoName,
- Ref: refName,
- Base: filePath,
- Crumbs: breadcrumbs(repoName, refName, filePath),
- Content: content,
+ Repo: repoName,
+ RefLabel: refLabel,
+ Base: filePath,
+ Crumbs: breadcrumbs(repoName, refLabel, filePath),
+ Content: content,
}
serve(w, "blob", data)
}
@@ -236,6 +366,8 @@
type TreeData struct {
Repo string
Ref string
+ RefLabel string
+ Commit string
HeadRef string
Base string
Crumbs []Crumb
@@ -250,7 +382,15 @@ Path string
IsDir bool
}
-func resolve(repo *git.Repository, refName string) (*plumbing.Reference, error) {
+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 {
@@ -283,24 +423,15 @@ if err != nil {
http.Error(w, err.Error(), 500)
return
}
- refName, treePath := parse(repo, r.PathValue("path"))
+ 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 refName == headRef && treePath == "" {
+ if commit.RefName == headRef && treePath == "" {
repoIndex(w, r)
- return
- }
- ref, err := resolve(repo, refName)
- 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()
@@ -350,12 +481,19 @@ }
}
entries = append(entries, em)
}
+ var refLabel string
+ if commit.RefName != "" {
+ refLabel = commit.RefName
+ } else {
+ refLabel = commit.Hash.String()
+ }
data := TreeData{
Repo: repoName,
- Ref: refName,
+ RefLabel: refLabel,
+ Ref: commit.RefName,
HeadRef: headRef,
Base: treePath,
- Crumbs: breadcrumbs(repoName, refName, treePath),
+ Crumbs: breadcrumbs(repoName, refLabel, treePath),
Entries: entries,
ReadmeName: readmeName,
Readme: readme,
@@ -369,7 +507,7 @@ URL string
IsLast bool
}
-func breadcrumbs(repoName string, refName string, p string) []Crumb {
+func breadcrumbs(repoName string, refLabel string, p string) []Crumb {
var crumbs []Crumb
last := true
cur := p
@@ -380,7 +518,7 @@ break
}
crumbs = append(crumbs, Crumb{
Name: base,
- URL: path.Join(repoName, "tree", refName, cur),
+ URL: path.Join(repoName, "tree", refLabel, cur),
IsLast: last,
})
last = false
diff --git a/templates/blob.tmpl b/templates/blob.tmpl
index f0be0b16f928f0d16c7948dfee3ff5ed99ea57a9..90f900a4ddb0dede3d5cf6345f6c845aacff0fed 100644
--- a/templates/blob.tmpl
+++ b/templates/blob.tmpl
@@ -1,6 +1,6 @@
{{define "title"}}{{.Repo}}/{{.Base}}{{end}}
{{define "content"}}
-<h1>{{if eq .Base ""}}{{.Repo}}{{else}}<a href="/{{.Repo}}/tree/{{.Ref}}">{{.Repo}}</a> /{{end}}
+<h1>{{if eq .Base ""}}{{.Repo}}{{else}}<a href="/{{.Repo}}/tree/{{.RefLabel}}">{{.Repo}}</a> /{{end}}
{{range $i, $c := .Crumbs}}
{{if $i}} / {{end}}
{{if $c.IsLast}}
@@ -9,7 +9,7 @@ {{else}}
<a href="/{{$c.URL}}">{{$c.Name}}</a>
{{end}}
{{end}}</h1>
-<p>@ {{.Ref}}</p>
+<p>@ {{.RefLabel}}</p>
<hr>
<pre>{{.Content}}</pre>
{{end}}
diff --git a/templates/commit.tmpl b/templates/commit.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..20f010e4b9e89a2a8971baa1897855df4f986fd9
--- /dev/null
+++ b/templates/commit.tmpl
@@ -0,0 +1,16 @@
+{{define "title"}}{{.Repo}} @ {{.Hash}}{{end}}
+{{define "content"}}
+<h1><a href="/{{.Repo}}">{{.Repo}}</a></h1>
+<p><a href="/">index</a> | <a href="/{{.Repo}}/refs">refs</a></p>
+<h4>commit {{.Hash}} | <a href="/{{.Repo}}/tree/{{.Hash}}">tree</a></h4>
+ <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}}
+{{end}}
+
+{{define "commit"}}
+{{template "base" .}}
+{{end}}
\ No newline at end of file
diff --git a/templates/commits.tmpl b/templates/commits.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..fbe0073965e503944b486492176ffa8cdd0a1b4e
--- /dev/null
+++ b/templates/commits.tmpl
@@ -0,0 +1,17 @@
+{{define "title"}}{{.Repo}} @ {{.Ref}}{{end}}
+{{define "content"}}
+<h1><a href="/{{.Repo}}/tree/{{.Ref}}">{{.Repo}}</a></h1>
+<p>@ {{.Ref}}</p>
+<h2>commits</h2>
+{{range .Commits}}
+ <p><a href="/{{$.Repo}}/commit/{{.Hash}}">{{.Hash}}</a></p>
+ <p>{{.Author}}</p>
+ <p>{{.When}}</p>
+ <pre>{{.Message}}</pre>
+ <br>
+{{end}}
+{{end}}
+
+{{define "commits"}}
+{{template "base" .}}
+{{end}}
\ No newline at end of file
diff --git a/templates/repo.tmpl b/templates/repo.tmpl
index f9c52d3da9d160d0639b71bd4dd71640632816bd..15ab581d855d323b3223801b752d209fb5332cbb 100644
--- a/templates/repo.tmpl
+++ b/templates/repo.tmpl
@@ -2,7 +2,7 @@ {{define "title"}}{{.Repo}}/{{.Base}}{{end}}
{{define "content"}}
<h1>{{.Repo}}</h1>
<p>@ {{.Ref}}</p>
-<p><a href="/">index</a> | <a href="/{{.Repo}}/refs">refs</a></p>
+<p><a href="/">index</a> | <a href="/{{.Repo}}/refs">refs</a> | <a href="/{{.Repo}}/commits/{{.Ref}}">commits</a></p>
<ul>
{{range .Entries}}
<li>
diff --git a/templates/tree.tmpl b/templates/tree.tmpl
index 1e1740a3d634c3dbf30d1b17bd3dbfc309a0cad3..16bad38dd41285f8e2c50864e98ee0e2e67732d4 100644
--- a/templates/tree.tmpl
+++ b/templates/tree.tmpl
@@ -1,6 +1,6 @@
{{define "title"}}{{.Repo}}/{{.Base}}{{end}}
{{define "content"}}
-<h1>{{if ne .Base ""}}<a href="/{{.Repo}}/tree/{{.Ref}}">{{.Repo}}</a> /
+<h1>{{if ne .Base ""}}<a href="/{{.Repo}}/tree/{{.RefLabel}}">{{.Repo}}</a> /
{{else}}
<a href="/{{.Repo}}/tree/{{.HeadRef}}">{{.Repo}}</a>
{{end}}
@@ -12,17 +12,17 @@ {{else}}
<a href="/{{$c.URL}}">{{$c.Name}}</a>
{{end}}
{{end}}</h1>
-<p>@ {{.Ref}}</p>
+<p>@ {{.RefLabel}}</p>
{{if eq .Base ""}}
-<p><a href="/">index</a> | <a href="/{{.Repo}}/refs">refs</a></p>
+<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>
{{range .Entries}}
<li>
{{- if .IsDir -}}
- 📁 <a href="/{{$.Repo}}/tree/{{$.Ref}}/{{.Path}}">{{.Name}}</a>
+ 📁 <a href="/{{$.Repo}}/tree/{{$.RefLabel}}/{{.Path}}">{{.Name}}</a>
{{- else -}}
- 📄 <a href="/{{$.Repo}}/blob/{{$.Ref}}/{{.Path}}">{{.Name}}</a>
+ 📄 <a href="/{{$.Repo}}/blob/{{$.RefLabel}}/{{.Path}}">{{.Name}}</a>
{{- end -}}
</li>
{{end}}