From 7e09bac9cce3fc6a2f744576291f19af989d4c5c Mon Sep 17 00:00:00 2001 From: Amolith Date: Thu, 6 Nov 2025 19:20:21 -0700 Subject: [PATCH] feat(webui): add individual tag detail pages Implement /{repo}/tag/{tag} route displaying tag metadata including name, authorship, creation date, optional annotation message, and links to file tree and commit. Tag dates use tagger date for annotated tags, falling back to commit author date for lightweight tags, matching the tags list behavior for consistency. Implements: a0daef3 Assisted-by: Claude Sonnet 4.5 via Crush --- pkg/web/templates/tag.html | 49 ++++++++++++ pkg/web/templates/tags.html | 2 +- pkg/web/webui.go | 4 + pkg/web/webui_tag.go | 143 ++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 pkg/web/templates/tag.html create mode 100644 pkg/web/webui_tag.go diff --git a/pkg/web/templates/tag.html b/pkg/web/templates/tag.html new file mode 100644 index 0000000000000000000000000000000000000000..7170b3852478c502ea0b7cfcd7da0af7a19ddfb9 --- /dev/null +++ b/pkg/web/templates/tag.html @@ -0,0 +1,49 @@ +{{define "content"}} + + +
+
+

{{.Ref.Name.Short}}

+ {{if .Commit}} +

+ {{ $names := .Commit | attributionNames }} + {{ $nlen := len $names }} + {{ range $i, $n := $names }} + {{ if gt $i 0 }} + {{ if eq $nlen 2 }} and {{ else if eq $i (sub $nlen 1) }}, and {{ else }}, {{ end }} + {{ end }} + {{$n}} + {{ end }} + created +

+ {{end}} +
+ + + + {{if .Tag}} + {{$message := .Tag.Message}} + {{if $message}} +
{{$message}}
+ {{end}} + {{else if .Commit}} + {{$body := .Commit.Message | commitBody}} + {{if $body}} +
{{$body}}
+ {{end}} + {{end}} +
+{{end}} diff --git a/pkg/web/templates/tags.html b/pkg/web/templates/tags.html index b23163dacb506b7cabe530feda3363a2accbcdcd..7813f16f9a3ab01d3df229177e36d084dbe376ac 100644 --- a/pkg/web/templates/tags.html +++ b/pkg/web/templates/tags.html @@ -4,7 +4,7 @@ {{if .Tags}} {{range .Tags}}
-

{{.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) +}