@@ -0,0 +1,49 @@
+{{define "content"}}
+<nav aria-label="breadcrumb">
+ <ul>
+ <li><a href="/{{.Repo.Name}}">{{.Repo.Name}}</a></li>
+ <li><a href="/{{.Repo.Name}}/tags">Tags</a></li>
+ <li aria-current="page">{{.Ref.Name.Short}}</li>
+ </ul>
+</nav>
+
+<header>
+ <hgroup>
+ <h2 id="tag-heading">{{.Ref.Name.Short}}</h2>
+ {{if .Commit}}
+ <p>
+ {{ $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 }}
+ <strong>{{$n}}</strong>
+ {{ end }}
+ created <time datetime="{{.TagDate | rfc3339}}" data-tooltip="{{.TagDate | formatDate}}">{{.TagDate | relativeTime}}</time>
+ </p>
+ {{end}}
+ </hgroup>
+
+ <nav aria-label="Tag actions">
+ <ul>
+ <li><a href="/{{.Repo.Name}}/tree/{{.Ref.Name.Short}}">Browse file tree</a></li>
+ {{if .Commit}}
+ <li><a href="/{{.Repo.Name}}/commit/{{.Commit.ID}}">View commit</a></li>
+ {{end}}
+ </ul>
+ </nav>
+
+ {{if .Tag}}
+ {{$message := .Tag.Message}}
+ {{if $message}}
+ <pre><code>{{$message}}</code></pre>
+ {{end}}
+ {{else if .Commit}}
+ {{$body := .Commit.Message | commitBody}}
+ {{if $body}}
+ <pre><code>{{$body}}</code></pre>
+ {{end}}
+ {{end}}
+</header>
+{{end}}
@@ -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)
@@ -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)
+}