Goit

Simple and lightweight Git web server
git clone https://git.omkov.net/Goit
git clone [email protected]:Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2025-01-11 02:40:24
Commitc850c86e98d13b282f071892f73b83a609ea926c
Parent37a8bcfb6e00c3dc44d0ed18a95349500b35fc00

Add a repo tag page with annotations

Diffstat

M README.md | 2 +-
M res/repo/refs.html | 2 +-
A res/repo/tag.html | 19 +++++++++++++++++++
M res/res.go | 3 +++
M src/goit/http.go | 1 +
M src/main.go | 4 ++++
M src/repo/commit.go | 18 +++++++++---------
M src/repo/refs.go | 20 ++++++++++----------
A src/repo/tag.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

9 files changed, 166 insertions, 21 deletions

diff --git a/README.md b/README.md
index 64d9832..3337143 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Note that at present, compatibility between updates is not guaranteed.
 ## Features
 
 - Git Smart HTTP protocol (v2 only)
-- Git SSH protocol (planned)
+- Git SSH protocol (using an external SSH server)
 - Repository log, tree, refs, and commit viewers
 - File viewer with syntax highlighting
 - File log, blame, and raw views
diff --git a/res/repo/refs.html b/res/repo/refs.html
index 43d841a..baa11cf 100644
--- a/res/repo/refs.html
+++ b/res/repo/refs.html
@@ -42,7 +42,7 @@
 				<tbody>
 				{{range .Tags}}
 					<tr>
-						<td>{{.Name}}</td>
+						<td><a href="/{{$.Name}}/tag/{{.Name}}">{{.Name}}</a></td>
 						<td><a href="/{{$.Name}}/commit/{{.Hash}}">{{.Message}}</a></td>
 						<td>{{.Author}}</td>
 						<td>{{.LastCommit}}</td>
diff --git a/res/repo/tag.html b/res/repo/tag.html
new file mode 100644
index 0000000..3f6693d
--- /dev/null
+++ b/res/repo/tag.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>{{template "base/head" .}}</head>
+	<body>
+		<header>{{template "repo/header" .}}</header><hr>
+		<main>
+			<table>
+				<tr><td>Tag</td><td><a href="/{{.Name}}/tag/{{.Tag}}">{{.Tag}}</a></td></tr>
+				<tr><td>Author</td><td>{{.Author}}</td></tr>
+				<tr><td>Date</td><td>{{.Date}}</td></tr>
+				<tr><td>Commit</td><td><a href="/{{.Name}}/commit/{{.Commit}}">{{.Commit}}</a></td></tr>
+				{{range $i, $h := .Parents}}
+					<tr><td>Parent</td><td><a href="/{{$.Name}}/commit/{{$h}}">{{$h}}</a></td></tr>
+				{{end}}
+			</table>
+			<p>{{.Message}}</p>
+		</main>
+	</body>
+</html>
diff --git a/res/res.go b/res/res.go
index 23d3ace..38c57b7 100644
--- a/res/res.go
+++ b/res/res.go
@@ -70,6 +70,9 @@ var RepoLog string
 //go:embed repo/commit.html
 var RepoCommit string
 
+//go:embed repo/tag.html
+var RepoTag string
+
 //go:embed repo/tree.html
 var RepoTree string
 
diff --git a/src/goit/http.go b/src/goit/http.go
index 2fbddc8..ff925b2 100644
--- a/src/goit/http.go
+++ b/src/goit/http.go
@@ -40,6 +40,7 @@ func init() {
 
 	template.Must(Tmpl.New("repo/log").Parse(res.RepoLog))
 	template.Must(Tmpl.New("repo/commit").Parse(res.RepoCommit))
+	template.Must(Tmpl.New("repo/tag").Parse(res.RepoTag))
 	template.Must(Tmpl.New("repo/tree").Parse(res.RepoTree))
 	template.Must(Tmpl.New("repo/file").Parse(res.RepoFile))
 	template.Must(Tmpl.New("repo/blame").Parse(res.RepoBlame))
diff --git a/src/main.go b/src/main.go
index 468c8f4..1c728cf 100644
--- a/src/main.go
+++ b/src/main.go
@@ -224,6 +224,10 @@ found:
 			rctx.URLParams.Add("hash", hash)
 			protect(http.HandlerFunc(repo.HandleCommit)).ServeHTTP(w, r)
 
+		case strings.HasPrefix(spath, "/tag/"):
+			rctx.URLParams.Add("tag", strings.TrimPrefix(spath, "/tag/"))
+			protect(http.HandlerFunc(repo.HandleTag)).ServeHTTP(w, r)
+
 		case strings.HasPrefix(spath, "/tree"):
 			rctx.URLParams.Add("*", strings.TrimLeft(strings.TrimPrefix(spath, "/tree"), "/"))
 			protect(http.HandlerFunc(repo.HandleTree)).ServeHTTP(w, r)
diff --git a/src/repo/commit.go b/src/repo/commit.go
index 423791e..35000b0 100644
--- a/src/repo/commit.go
+++ b/src/repo/commit.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"html/template"
-	"log"
 	"net/http"
 	"path/filepath"
 	"strconv"
@@ -15,6 +14,7 @@ import (
 	"time"
 
 	"github.com/Jamozed/Goit/src/goit"
+	"github.com/Jamozed/Goit/src/util"
 	"github.com/buildkite/terminal-to-html/v3"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-git/go-git/v5"
@@ -24,7 +24,7 @@ import (
 func HandleCommit(w http.ResponseWriter, r *http.Request) {
 	auth, user, err := goit.Auth(w, r, true)
 	if err != nil {
-		log.Println("[/repo/commit]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -53,13 +53,13 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 		Summary                     string
 		Diff                        template.HTML
 	}{
-		Title:        repo.Name + " - Log",
 		HeaderFields: GetHeaderFields(auth, user, repo, r.Host),
+		Title:        repo.Name + " - Log",
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true))
 	if err != nil {
-		log.Println("[/repo/commit]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -67,7 +67,7 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 	ref, err := gr.Head()
 	if err != nil {
 		if !errors.Is(err, plumbing.ErrReferenceNotFound) {
-			log.Println("[/repo/log]", err.Error())
+			util.PrintFuncError(err)
 			goit.HttpError(w, http.StatusInternalServerError)
 			return
 		}
@@ -85,7 +85,7 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 		goit.HttpError(w, http.StatusNotFound)
 		return
 	} else if err != nil {
-		log.Println("[/repo/commit]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -102,7 +102,7 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 
 	st, err := goit.DiffStats(commit)
 	if err != nil {
-		log.Println("[/repo/commit]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -146,7 +146,7 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 	c.Dir = goit.RepoPath(repo.Name, true)
 	out, _, err := c.Run(nil, nil)
 	if err != nil {
-		log.Println("[/repo/commit]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -154,6 +154,6 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 	data.Diff = template.HTML(terminal.Render(out))
 
 	if err := goit.Tmpl.ExecuteTemplate(w, "repo/commit", data); err != nil {
-		log.Println("[/repo/commit]", err.Error())
+		util.PrintFuncError(err)
 	}
 }
diff --git a/src/repo/refs.go b/src/repo/refs.go
index 5f87bc5..73abad5 100644
--- a/src/repo/refs.go
+++ b/src/repo/refs.go
@@ -5,7 +5,6 @@ package repo
 
 import (
 	"errors"
-	"log"
 	"net/http"
 	"path/filepath"
 	"slices"
@@ -13,6 +12,7 @@ import (
 	"time"
 
 	"github.com/Jamozed/Goit/src/goit"
+	"github.com/Jamozed/Goit/src/util"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
@@ -22,7 +22,7 @@ import (
 func HandleRefs(w http.ResponseWriter, r *http.Request) {
 	auth, user, err := goit.Auth(w, r, true)
 	if err != nil {
-		log.Println("[admin]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -51,7 +51,7 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true))
 	if err != nil {
-		log.Println("[/repo/refs]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -59,7 +59,7 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 	ref, err := gr.Head()
 	if err != nil {
 		if !errors.Is(err, plumbing.ErrReferenceNotFound) {
-			log.Println("[/repo/log]", err.Error())
+			util.PrintFuncError(err)
 			goit.HttpError(w, http.StatusInternalServerError)
 			return
 		}
@@ -73,7 +73,7 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if iter, err := gr.Branches(); err != nil {
-		log.Println("[/repo/refs]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	} else if err := iter.ForEach(func(r *plumbing.Reference) error {
@@ -94,13 +94,13 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 
 		return nil
 	}); err != nil {
-		log.Println("[/repo/refs]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
 
 	if iter, err := gr.Tags(); err != nil {
-		log.Println("[/repo/refs]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	} else if err := iter.ForEach(func(r *plumbing.Reference) error {
@@ -124,12 +124,12 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 
 		data.Tags = append(data.Tags, row{
 			Name: r.Name().Short(), Message: strings.SplitN(c.Message, "\n", 2)[0], Author: c.Author.Name,
-			LastCommit: c.Author.When.UTC().Format(time.DateTime), Hash: r.Hash().String(),
+			LastCommit: c.Author.When.UTC().Format(time.DateTime), Hash: c.Hash.String(),
 		})
 
 		return nil
 	}); err != nil {
-		log.Println("[/repo/refs]", err.Error())
+		util.PrintFuncError(err)
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -137,6 +137,6 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 	slices.Reverse(data.Tags)
 
 	if err := goit.Tmpl.ExecuteTemplate(w, "repo/refs", data); err != nil {
-		log.Println("[/repo/refs]", err.Error())
+		util.PrintFuncError(err)
 	}
 }
diff --git a/src/repo/tag.go b/src/repo/tag.go
new file mode 100644
index 0000000..4142c19
--- /dev/null
+++ b/src/repo/tag.go
@@ -0,0 +1,118 @@
+// Copyright (C) 2025, Jakob Wakeling
+// All rights reserved.
+
+package repo
+
+import (
+	"errors"
+	"net/http"
+	"path/filepath"
+	"time"
+
+	"github.com/Jamozed/Goit/src/goit"
+	"github.com/Jamozed/Goit/src/util"
+	"github.com/go-chi/chi/v5"
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
+)
+
+func HandleTag(w http.ResponseWriter, r *http.Request) {
+	auth, user, err := goit.Auth(w, r, true)
+	if err != nil {
+		util.PrintFuncError(err)
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	repo, err := goit.GetRepoByName(chi.URLParam(r, "repo"))
+	if err != nil {
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else if repo == nil || !goit.IsVisible(repo, auth, user) {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	data := struct {
+		HeaderFields
+		Title                     string
+		Tag, Author, Date, Commit string
+		Parents                   []string
+		Message                   string
+	}{
+		HeaderFields: GetHeaderFields(auth, user, repo, r.Host),
+		Title:        repo.Name + " - Tags",
+	}
+
+	gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true))
+	if err != nil {
+		util.PrintFuncError(err)
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	head, err := gr.Head()
+	if err != nil {
+		if !errors.Is(err, plumbing.ErrReferenceNotFound) {
+			util.PrintFuncError(err)
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+	} else {
+		if readme, _ := findPattern(gr, head, readmePattern); readme != "" {
+			data.Readme = filepath.Join("/", repo.Name, "file", readme)
+		}
+		if licence, _ := findPattern(gr, head, licencePattern); licence != "" {
+			data.Licence = filepath.Join("/", repo.Name, "file", licence)
+		}
+	}
+
+	ref, err := gr.Tag(chi.URLParam(r, "tag"))
+	if errors.Is(err, plumbing.ErrReferenceNotFound) {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	} else if err != nil {
+		util.PrintFuncError(err)
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	data.Tag = ref.Name().Short()
+
+	var commit *object.Commit
+	if tag, err := gr.TagObject(ref.Hash()); err != nil {
+		if !errors.Is(err, plumbing.ErrObjectNotFound) {
+			util.PrintFuncError(err)
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+
+		/* Tag is not annotated. */
+		if commit, err = gr.CommitObject(ref.Hash()); err != nil {
+			util.PrintFuncError(err)
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+
+		data.Author = commit.Author.String()
+		data.Message = commit.Message
+	} else {
+		/* Tag is annotated. */
+		if commit, err = gr.CommitObject(tag.Target); err != nil {
+			util.PrintFuncError(err)
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+
+		data.Author = tag.Tagger.String()
+		data.Message = tag.Message
+	}
+
+	data.Date = commit.Author.When.UTC().Format(time.DateTime)
+	data.Commit = commit.Hash.String()
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "repo/tag", data); err != nil {
+		util.PrintFuncError(err)
+	}
+}