{{.Ref.Name.Short}}
+{{.Ref.Name.Short}}
{{if .TagMessage}}{{.TagMessage}}
{{end}}
diff --git a/pkg/web/webui.go b/pkg/web/webui.go
index 51eb8ea96cc28071998d37f9ff95be50c93b6644..a0d7e51918c609b53896e91545fd621f2a007283 100644
--- a/pkg/web/webui.go
+++ b/pkg/web/webui.go
@@ -365,6 +365,10 @@ func WebUIController(ctx context.Context, r *mux.Router) {
r.Handle(basePrefix+"/tags", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTags)))).
Methods(http.MethodGet)
+ // Tag detail route
+ r.Handle(basePrefix+"/tag/{tag:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTag)))).
+ Methods(http.MethodGet)
+
// Repository overview (catch-all, must be last)
r.Handle(basePrefix, withRepoVars(withWebUIAccess(http.HandlerFunc(repoOverview)))).
Methods(http.MethodGet)
diff --git a/pkg/web/webui_tag.go b/pkg/web/webui_tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..64baad5389b3726dfe7c8dba903ca9aa39aad97e
--- /dev/null
+++ b/pkg/web/webui_tag.go
@@ -0,0 +1,143 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/log/v2"
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/pkg/config"
+ "github.com/charmbracelet/soft-serve/pkg/proto"
+ "github.com/gorilla/mux"
+)
+
+// TagData contains data for rendering individual tag view.
+type TagData struct {
+ RepoBaseData
+ Ref *git.Reference
+ Tag *git.Tag
+ Commit *git.Commit
+ TagDate time.Time
+}
+
+func repoTag(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ logger := log.FromContext(ctx)
+ cfg := config.FromContext(ctx)
+ repo := proto.RepositoryFromContext(ctx)
+ vars := mux.Vars(r)
+ tagName := vars["tag"]
+
+ gr, err := openRepository(repo)
+ if err != nil {
+ logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
+ renderInternalServerError(w, r)
+ return
+ }
+
+ // Check if tag exists
+ if !gr.HasTag(tagName) {
+ logger.Debug("tag not found", "repo", repo.Name(), "tag", tagName)
+ renderNotFound(w, r)
+ return
+ }
+
+ // Get all references and find our tag
+ refs, err := gr.References()
+ if err != nil {
+ logger.Debug("failed to get references", "repo", repo.Name(), "err", err)
+ renderInternalServerError(w, r)
+ return
+ }
+
+ var ref *git.Reference
+ for _, r := range refs {
+ if r.IsTag() && r.Name().Short() == tagName {
+ ref = r
+ break
+ }
+ }
+
+ if ref == nil {
+ logger.Debug("tag reference not found", "repo", repo.Name(), "tag", tagName)
+ renderNotFound(w, r)
+ return
+ }
+
+ var tag *git.Tag
+ var commit *git.Commit
+ var tagDate time.Time
+
+ // Try to get annotated tag
+ tag, err = gr.Tag(tagName)
+ if err == nil && tag != nil {
+ // Annotated tag - get tagger date
+ if tagger := tag.Tagger(); tagger != nil {
+ tagDate = tagger.When
+ }
+ // Get the commit the tag points to via the hash
+ commitHash := ref.ID
+ commit, err = gr.CatFileCommit(commitHash)
+ if err != nil {
+ logger.Debug("failed to get commit from tag", "repo", repo.Name(), "tag", tagName, "err", err)
+ renderInternalServerError(w, r)
+ return
+ }
+ } else {
+ // Lightweight tag - points directly to commit
+ commit, err = gr.CatFileCommit(ref.ID)
+ if err != nil {
+ logger.Debug("failed to get commit", "repo", repo.Name(), "tag", tagName, "err", err)
+ renderNotFound(w, r)
+ return
+ }
+ // Use commit author date for lightweight tags
+ tagDate = commit.Author.When
+ }
+
+ // Fallback to commit date if tag date not available
+ if tagDate.IsZero() && commit != nil {
+ tagDate = commit.Author.When
+ }
+
+ defaultBranch := getDefaultBranch(gr)
+
+ repoDisplayName := repo.ProjectName()
+ if repoDisplayName == "" {
+ repoDisplayName = repo.Name()
+ }
+
+ // Build description from tag message or commit subject
+ description := ""
+ if tag != nil && tag.Message() != "" {
+ description = tag.Message()
+ } else if commit != nil {
+ lines := strings.Split(commit.Message, "\n")
+ if len(lines) > 0 {
+ description = lines[0]
+ }
+ }
+ if len(description) > 200 {
+ description = description[:200] + "..."
+ }
+
+ data := TagData{
+ RepoBaseData: RepoBaseData{
+ BaseData: BaseData{
+ ServerName: cfg.Name,
+ ActiveTab: "tags",
+ Title: tagName + " | " + repoDisplayName,
+ Description: description,
+ },
+ Repo: repo,
+ DefaultBranch: defaultBranch,
+ },
+ Ref: ref,
+ Tag: tag,
+ Commit: commit,
+ TagDate: tagDate,
+ }
+
+ renderHTML(w, "tag.html", data)
+}