cerrado @ 44bc8e4078a09857ad86691a83e7ba7d4e3a69c4

ref: Simplify path builder code
diff --git a/README.md b/README.md
index e49e6bc371427c1555d6339562a74b0b66f0bf80..bd5e69eb48b984a8f2648b4e5474352d62a015fa 100644
--- a/README.md
+++ b/README.md
@@ -23,8 +23,6 @@ To run the project you just need to do a make run.
 
 ### TODO
 
-- Add path to tree view
-    - Fix href with extra slash
 - Add message to tags
 - Add link to tar browser from commit page
 - Add patch to the commit page
diff --git a/pkg/git/git.go b/pkg/git/git.go
index 6b58d35cd3208ba2d12937ad1b31baca0c8ad78b..7341c1bc3f04e6d91f17ac877bb523afd5ee19e2 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -38,6 +38,11 @@ 		mode    fs.FileMode
 		modTime time.Time
 		isDir   bool
 	}
+
+	TagReference struct {
+		ref *plumbing.Reference
+		tag *object.Tag
+	}
 )
 
 func OpenRepository(dir string) (*GitRepository, error) {
@@ -119,18 +124,31 @@ func (g *GitRepository) Head() (*plumbing.Reference, error) {
 	return g.repository.Head()
 }
 
-func (g *GitRepository) Tags() ([]*plumbing.Reference, error) {
-	ti, err := g.repository.Tags()
+func (g *GitRepository) Tags() ([]*TagReference, error) {
+	iter, err := g.repository.Tags()
 	if err != nil {
 		return nil, err
 	}
 
-	tags := []*plumbing.Reference{}
-	err = ti.ForEach(func(t *plumbing.Reference) error {
-		tags = append(tags, t)
+	tags := make([]*TagReference, 0)
+
+	if err := iter.ForEach(func(ref *plumbing.Reference) error {
+		obj, err := g.repository.TagObject(ref.Hash())
+		switch err {
+		case nil:
+			tags = append(tags, &TagReference{
+				ref: ref,
+				tag: obj,
+			})
+		case plumbing.ErrObjectNotFound:
+			tags = append(tags, &TagReference{
+				ref: ref,
+			})
+		default:
+			return err
+		}
 		return nil
-	})
-	if err != nil {
+	}); err != nil {
 		return nil, err
 	}
 
@@ -361,3 +379,19 @@
 func (i *infoWrapper) Sys() any {
 	return nil
 }
+
+func (t *TagReference) HashString() string {
+	return t.ref.Hash().String()
+}
+
+func (t *TagReference) ShortName() string {
+	return t.ref.Name().Short()
+}
+
+func (t *TagReference) Message() string {
+	if t.tag != nil {
+		return t.tag.Message
+	}
+	return ""
+
+}
diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
index 5e5012263de12db42cb7457a21a2fad99ee50b89..9549f0e414d81baa03f6c1e385ea5c872c0e31f5 100644
--- a/pkg/handler/git/handler.go
+++ b/pkg/handler/git/handler.go
@@ -18,7 +18,6 @@ 	"github.com/alecthomas/chroma/v2"
 	"github.com/alecthomas/chroma/v2/formatters/html"
 	"github.com/alecthomas/chroma/v2/lexers"
 	"github.com/alecthomas/chroma/v2/styles"
-	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/gomarkdown/markdown"
 	markdownhtml "github.com/gomarkdown/markdown/html"
@@ -27,30 +26,16 @@ )
 
 type (
 	GitHandler struct {
-		gitService gitService
+		gitService *service.GitService
 		readmePath string
 	}
 
-	gitService interface {
-		ListRepositories() ([]*service.Repository, error)
-		ListCommits(name string, ref string, count int) ([]*object.Commit, error)
-		LastCommit(name string, ref string) (*object.Commit, error)
-		GetHead(name string) (*plumbing.Reference, error)
-		GetTree(name, ref, path string) (*object.Tree, error)
-		IsBinary(name, ref, path string) (bool, error)
-		GetFileContent(name, ref, path string) ([]byte, error)
-		GetAbout(name string) ([]byte, error)
-		ListTags(name string) ([]*plumbing.Reference, error)
-		ListBranches(name string) ([]*plumbing.Reference, error)
-		WriteTarGZip(w io.Writer, name, ref, prefix string) error
-	}
-
 	configurationRepository interface {
 		GetRootReadme() string
 	}
 )
 
-func NewGitHandler(gitService gitService, confRepo configurationRepository) *GitHandler {
+func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler {
 	return &GitHandler{
 		gitService: gitService,
 		readmePath: confRepo.GetRootReadme(),
diff --git a/pkg/service/git.go b/pkg/service/git.go
index df4e3aa36917ce74895247c02a4f2a4f39f868b8..b368f0cc321b64d593bd2399e86c5de8d43c77f7 100644
--- a/pkg/service/git.go
+++ b/pkg/service/git.go
@@ -217,7 +217,7 @@
 	return file, nil
 }
 
-func (g *GitService) ListTags(name string) ([]*plumbing.Reference, error) {
+func (g *GitService) ListTags(name string) ([]*git.TagReference, error) {
 	r := g.configRepo.GetByName(name)
 	if r == nil {
 		return nil, ErrRepositoryNotFound
diff --git a/pkg/u/file.go b/pkg/u/file.go
index fafe0fb00f4b30262594af5ab6b32873515301bb..5010b3eef2a8ea0c37b6bbeb23b7ab721197a93c 100644
--- a/pkg/u/file.go
+++ b/pkg/u/file.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"log/slog"
 	"os"
-	"path/filepath"
+	"strings"
 )
 
 func FileExist(filename string) bool {
@@ -22,21 +22,43 @@ 	}
 }
 
 // This is just a slin wraper to make easier to compose path in the template
-type Pathing string
+type Pathing struct {
+	sb strings.Builder
+}
 
-func Root() Pathing {
-	return "/"
+func NewPathing() *Pathing {
+	return &Pathing{}
 }
 
-func (s Pathing) AddPath(p string) Pathing {
-	return Pathing(filepath.Join(string(s), p))
+func (s *Pathing) AddPath(p string) *Pathing {
+	if len(p) == 0 {
+		return s
+	}
+
+	// if it has trailing / remove it
+	if p[len(p)-1] == '/' {
+		p = p[:len(p)-1]
+		return s.AddPath(p)
+	}
+
+	// if it does not have it so add
+	if p[0] == '/' {
+		s.sb.WriteString(p)
+	} else {
+		s.sb.WriteString("/" + p)
+	}
+
+	return s
 }
 
-func (s Pathing) AddPaths(p []string) Pathing {
-	f := filepath.Join(p...)
-	return Pathing(filepath.Join(string(s), f))
+func (s *Pathing) AddPaths(p []string) *Pathing {
+	for _, v := range p {
+		s.AddPath(v)
+	}
+
+	return s
 }
 
-func (s Pathing) Done() string {
-	return string(s)
+func (s *Pathing) Done() string {
+	return s.sb.String()
 }
diff --git a/pkg/u/file_test.go b/pkg/u/file_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b7d69752f1a0d9d7e6b0e7c828a417e1f8162b15
--- /dev/null
+++ b/pkg/u/file_test.go
@@ -0,0 +1,59 @@
+// go:build unit
+package u
+
+import "testing"
+
+func TestPathing(t *testing.T) {
+	testCases := []struct {
+		name string
+		in   []any
+		out  string
+	}{
+		{
+			name: "root",
+			in:   []any{},
+			out:  "",
+		},
+		{
+			name: "empty",
+			in: []any{
+				"/",
+				[]string{"/", "/"},
+				"/",
+				[]string{"/"},
+			},
+			out: "",
+		},
+		{
+			name: "empty",
+			in: []any{
+				"usr",
+				[]string{"/share/", "lib"},
+				"/demo",
+				[]string{"/out//"},
+			},
+			out: "/usr/share/lib/demo/out",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			r := NewPathing()
+
+			for _, v := range tc.in {
+				switch s := v.(type) {
+				case string:
+					r = r.AddPath(s)
+				case []string:
+					r = r.AddPaths(s)
+				}
+			}
+
+			path := r.Done()
+			if tc.out != path {
+				t.Errorf("String mismatch: wanted %s got %s", tc.out, path)
+			}
+
+		})
+	}
+}
diff --git a/templates/gititemrefs.qtpl b/templates/gititemrefs.qtpl
index 624408297c9680822ebf314fd7fb955e823ab606..9f4f74b4c0e7b99f398e79238b3f049b8847b62f 100644
--- a/templates/gititemrefs.qtpl
+++ b/templates/gititemrefs.qtpl
@@ -1,8 +1,9 @@
 {% import "github.com/go-git/go-git/v5/plumbing" %}
+{% import "git.gabrielgio.me/cerrado/pkg/git" %}
 
 {% code
 type GitItemRefsPage struct {
-    Tags []*plumbing.Reference
+    Tags []*git.TagReference
     Branches []*plumbing.Reference
 }
 %}
@@ -12,26 +13,7 @@
 {% func (g *GitItemRefsPage) GitContent(name, ref string) %}
 <div class="row">
   <div class="col-md-8">
-    {% if len(g.Tags) > 0 %}
-    <div class="event-list">
-      {% for _, t := range g.Tags %}
-      <div class="row event me-md-2">
-          <div class="col-4">
-           {%s t.Name().Short() %}
-          </div>
-          <div class="col-8">
-            <div class="float-end">
-              <a href="/{%s name %}/archive/{%s t.Name().Short() %}.tar.gz">tar.gz</a>
-              <a href="/{%s name %}/tree/{%s t.Name().Short() %}/">tree</a>
-              <a href="/{%s name %}/log/{%s t.Name().Short() %}/">log</a>
-            </div>
-          </div>
-      </div>
-      {% endfor %}
-    </div>
-    {% else %}
-        <p> No tags </p>
-    {% endif %}
+    {%= ListTags(name, g.Tags) %}
   </div>
   <div class="col-md-4">
     <div class="event-list">
diff --git a/templates/gititemrefs.qtpl.go b/templates/gititemrefs.qtpl.go
index da9bfe757f4d6bc91fb3d53c65dc2cd0faa36074..d54301de94e72454f4fc6070f0f5fa906febf1c9 100644
--- a/templates/gititemrefs.qtpl.go
+++ b/templates/gititemrefs.qtpl.go
@@ -7,214 +7,154 @@
 //line gititemrefs.qtpl:1
 import "github.com/go-git/go-git/v5/plumbing"
 
-//line gititemrefs.qtpl:3
+//line gititemrefs.qtpl:2
+import "git.gabrielgio.me/cerrado/pkg/git"
+
+//line gititemrefs.qtpl:4
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line gititemrefs.qtpl:3
+//line gititemrefs.qtpl:4
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line gititemrefs.qtpl:4
+//line gititemrefs.qtpl:5
 type GitItemRefsPage struct {
-	Tags     []*plumbing.Reference
+	Tags     []*git.TagReference
 	Branches []*plumbing.Reference
 }
 
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 func (g *GitItemRefsPage) StreamNav(qw422016 *qt422016.Writer, name, ref string) {
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	StreamGitItemNav(qw422016, name, ref, Refs)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 }
 
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 func (g *GitItemRefsPage) WriteNav(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	g.StreamNav(qw422016, name, ref)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	qt422016.ReleaseWriter(qw422016)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 }
 
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 func (g *GitItemRefsPage) Nav(name, ref string) string {
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	qb422016 := qt422016.AcquireByteBuffer()
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	g.WriteNav(qb422016, name, ref)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	qs422016 := string(qb422016.B)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	qt422016.ReleaseByteBuffer(qb422016)
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 	return qs422016
-//line gititemrefs.qtpl:10
+//line gititemrefs.qtpl:11
 }
 
-//line gititemrefs.qtpl:12
+//line gititemrefs.qtpl:13
 func (g *GitItemRefsPage) StreamGitContent(qw422016 *qt422016.Writer, name, ref string) {
-//line gititemrefs.qtpl:12
+//line gititemrefs.qtpl:13
 	qw422016.N().S(`
 <div class="row">
   <div class="col-md-8">
     `)
-//line gititemrefs.qtpl:15
-	if len(g.Tags) > 0 {
-//line gititemrefs.qtpl:15
-		qw422016.N().S(`
-    <div class="event-list">
-      `)
-//line gititemrefs.qtpl:17
-		for _, t := range g.Tags {
-//line gititemrefs.qtpl:17
-			qw422016.N().S(`
-      <div class="row event me-md-2">
-          <div class="col-4">
-           `)
-//line gititemrefs.qtpl:20
-			qw422016.E().S(t.Name().Short())
-//line gititemrefs.qtpl:20
-			qw422016.N().S(`
-          </div>
-          <div class="col-8">
-            <div class="float-end">
-              <a href="/`)
-//line gititemrefs.qtpl:24
-			qw422016.E().S(name)
-//line gititemrefs.qtpl:24
-			qw422016.N().S(`/archive/`)
-//line gititemrefs.qtpl:24
-			qw422016.E().S(t.Name().Short())
-//line gititemrefs.qtpl:24
-			qw422016.N().S(`.tar.gz">tar.gz</a>
-              <a href="/`)
-//line gititemrefs.qtpl:25
-			qw422016.E().S(name)
-//line gititemrefs.qtpl:25
-			qw422016.N().S(`/tree/`)
-//line gititemrefs.qtpl:25
-			qw422016.E().S(t.Name().Short())
-//line gititemrefs.qtpl:25
-			qw422016.N().S(`/">tree</a>
-              <a href="/`)
-//line gititemrefs.qtpl:26
-			qw422016.E().S(name)
-//line gititemrefs.qtpl:26
-			qw422016.N().S(`/log/`)
-//line gititemrefs.qtpl:26
-			qw422016.E().S(t.Name().Short())
-//line gititemrefs.qtpl:26
-			qw422016.N().S(`/">log</a>
-            </div>
-          </div>
-      </div>
-      `)
-//line gititemrefs.qtpl:30
-		}
-//line gititemrefs.qtpl:30
-		qw422016.N().S(`
-    </div>
-    `)
-//line gititemrefs.qtpl:32
-	} else {
-//line gititemrefs.qtpl:32
-		qw422016.N().S(`
-        <p> No tags </p>
-    `)
-//line gititemrefs.qtpl:34
-	}
-//line gititemrefs.qtpl:34
+//line gititemrefs.qtpl:16
+	StreamListTags(qw422016, name, g.Tags)
+//line gititemrefs.qtpl:16
 	qw422016.N().S(`
   </div>
   <div class="col-md-4">
     <div class="event-list">
       `)
-//line gititemrefs.qtpl:38
+//line gititemrefs.qtpl:20
 	for _, b := range g.Branches {
-//line gititemrefs.qtpl:38
+//line gititemrefs.qtpl:20
 		qw422016.N().S(`
       <div class="row event">
           <div class="col-4">
            `)
-//line gititemrefs.qtpl:41
+//line gititemrefs.qtpl:23
 		qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:41
+//line gititemrefs.qtpl:23
 		qw422016.N().S(`
           </div>
           <div class="col-8">
             <div class="float-end">
               <a href="/`)
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:27
 		qw422016.E().S(name)
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:27
 		qw422016.N().S(`/archive/`)
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:27
 		qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:27
 		qw422016.N().S(`.tar.gz">tar.gz</a>
               <a href="/`)
-//line gititemrefs.qtpl:46
+//line gititemrefs.qtpl:28
 		qw422016.E().S(name)
-//line gititemrefs.qtpl:46
+//line gititemrefs.qtpl:28
 		qw422016.N().S(`/tree/`)
-//line gititemrefs.qtpl:46
+//line gititemrefs.qtpl:28
 		qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:46
+//line gititemrefs.qtpl:28
 		qw422016.N().S(`/">tree</a>
               <a href="/`)
-//line gititemrefs.qtpl:47
+//line gititemrefs.qtpl:29
 		qw422016.E().S(name)
-//line gititemrefs.qtpl:47
+//line gititemrefs.qtpl:29
 		qw422016.N().S(`/log/`)
-//line gititemrefs.qtpl:47
+//line gititemrefs.qtpl:29
 		qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:47
+//line gititemrefs.qtpl:29
 		qw422016.N().S(`/">log</a>
             </div>
           </div>
       </div>
       `)
-//line gititemrefs.qtpl:51
+//line gititemrefs.qtpl:33
 	}
-//line gititemrefs.qtpl:51
+//line gititemrefs.qtpl:33
 	qw422016.N().S(`
     </div>
   </div>
 </div>
 `)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 }
 
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 func (g *GitItemRefsPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	g.StreamGitContent(qw422016, name, ref)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	qt422016.ReleaseWriter(qw422016)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 }
 
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 func (g *GitItemRefsPage) GitContent(name, ref string) string {
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	qb422016 := qt422016.AcquireByteBuffer()
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	g.WriteGitContent(qb422016, name, ref)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	qs422016 := string(qb422016.B)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	qt422016.ReleaseByteBuffer(qb422016)
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 	return qs422016
-//line gititemrefs.qtpl:55
+//line gititemrefs.qtpl:37
 }
diff --git a/templates/gititemsummary.qtpl b/templates/gititemsummary.qtpl
index ef2c534df5b1d5ab85760c7de436b83d5f041e7c..44e160474b48574d3c93773fa3780a17529aa480 100644
--- a/templates/gititemsummary.qtpl
+++ b/templates/gititemsummary.qtpl
@@ -1,9 +1,10 @@
 {% import "github.com/go-git/go-git/v5/plumbing" %}
 {% import "github.com/go-git/go-git/v5/plumbing/object" %}
+{% import "git.gabrielgio.me/cerrado/pkg/git" %}
 
 {% code
 type GitItemSummaryPage struct {
-    Tags []*plumbing.Reference
+    Tags []*git.TagReference
     Branches []*plumbing.Reference
     Commits []*object.Commit
 }
@@ -14,26 +15,7 @@
 {% func (g *GitItemSummaryPage) GitContent(name, ref string) %}
 <div class="row">
   <div class="col-md-8">
-    {% if len(g.Tags) > 0 %}
-    <div class="event-list">
-      {% for _, t := range g.Tags %}
-      <div class="row event me-md-2">
-          <div class="col-4">
-           {%s t.Name().Short() %}
-          </div>
-          <div class="col-8">
-            <div class="float-end">
-              <a href="/{%s name %}/archive/{%s t.Name().Short() %}.tar.gz">tar.gz</a>
-              <a href="/{%s name %}/tree/{%s t.Name().Short() %}/">tree</a>
-              <a href="/{%s name %}/log/{%s t.Name().Short() %}/">log</a>
-            </div>
-          </div>
-      </div>
-      {% endfor %}
-    </div>
-    {% else %}
-        <p> No tags </p>
-    {% endif %}
+    {%= ListTags(name, g.Tags) %}
   </div>
   <div class="col-md-4">
     <div class="event-list">
diff --git a/templates/gititemsummary.qtpl.go b/templates/gititemsummary.qtpl.go
index 570a95501376383761ed6eeaf38d4e7d25c765ad..24fed9df74108f464aa3b1685e1fa597559620fc 100644
--- a/templates/gititemsummary.qtpl.go
+++ b/templates/gititemsummary.qtpl.go
@@ -10,185 +10,125 @@
 //line gititemsummary.qtpl:2
 import "github.com/go-git/go-git/v5/plumbing/object"
 
-//line gititemsummary.qtpl:4
+//line gititemsummary.qtpl:3
+import "git.gabrielgio.me/cerrado/pkg/git"
+
+//line gititemsummary.qtpl:5
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line gititemsummary.qtpl:4
+//line gititemsummary.qtpl:5
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line gititemsummary.qtpl:5
+//line gititemsummary.qtpl:6
 type GitItemSummaryPage struct {
-	Tags     []*plumbing.Reference
+	Tags     []*git.TagReference
 	Branches []*plumbing.Reference
 	Commits  []*object.Commit
 }
 
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 func (g *GitItemSummaryPage) StreamNav(qw422016 *qt422016.Writer, name, ref string) {
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	StreamGitItemNav(qw422016, name, ref, Summary)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 }
 
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 func (g *GitItemSummaryPage) WriteNav(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	g.StreamNav(qw422016, name, ref)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	qt422016.ReleaseWriter(qw422016)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 }
 
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 func (g *GitItemSummaryPage) Nav(name, ref string) string {
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	qb422016 := qt422016.AcquireByteBuffer()
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	g.WriteNav(qb422016, name, ref)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	qs422016 := string(qb422016.B)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	qt422016.ReleaseByteBuffer(qb422016)
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 	return qs422016
-//line gititemsummary.qtpl:12
+//line gititemsummary.qtpl:13
 }
 
-//line gititemsummary.qtpl:14
+//line gititemsummary.qtpl:15
 func (g *GitItemSummaryPage) StreamGitContent(qw422016 *qt422016.Writer, name, ref string) {
-//line gititemsummary.qtpl:14
+//line gititemsummary.qtpl:15
 	qw422016.N().S(`
 <div class="row">
   <div class="col-md-8">
     `)
-//line gititemsummary.qtpl:17
-	if len(g.Tags) > 0 {
-//line gititemsummary.qtpl:17
-		qw422016.N().S(`
-    <div class="event-list">
-      `)
-//line gititemsummary.qtpl:19
-		for _, t := range g.Tags {
-//line gititemsummary.qtpl:19
-			qw422016.N().S(`
-      <div class="row event me-md-2">
-          <div class="col-4">
-           `)
-//line gititemsummary.qtpl:22
-			qw422016.E().S(t.Name().Short())
-//line gititemsummary.qtpl:22
-			qw422016.N().S(`
-          </div>
-          <div class="col-8">
-            <div class="float-end">
-              <a href="/`)
-//line gititemsummary.qtpl:26
-			qw422016.E().S(name)
-//line gititemsummary.qtpl:26
-			qw422016.N().S(`/archive/`)
-//line gititemsummary.qtpl:26
-			qw422016.E().S(t.Name().Short())
-//line gititemsummary.qtpl:26
-			qw422016.N().S(`.tar.gz">tar.gz</a>
-              <a href="/`)
-//line gititemsummary.qtpl:27
-			qw422016.E().S(name)
-//line gititemsummary.qtpl:27
-			qw422016.N().S(`/tree/`)
-//line gititemsummary.qtpl:27
-			qw422016.E().S(t.Name().Short())
-//line gititemsummary.qtpl:27
-			qw422016.N().S(`/">tree</a>
-              <a href="/`)
-//line gititemsummary.qtpl:28
-			qw422016.E().S(name)
-//line gititemsummary.qtpl:28
-			qw422016.N().S(`/log/`)
-//line gititemsummary.qtpl:28
-			qw422016.E().S(t.Name().Short())
-//line gititemsummary.qtpl:28
-			qw422016.N().S(`/">log</a>
-            </div>
-          </div>
-      </div>
-      `)
-//line gititemsummary.qtpl:32
-		}
-//line gititemsummary.qtpl:32
-		qw422016.N().S(`
-    </div>
-    `)
-//line gititemsummary.qtpl:34
-	} else {
-//line gititemsummary.qtpl:34
-		qw422016.N().S(`
-        <p> No tags </p>
-    `)
-//line gititemsummary.qtpl:36
-	}
-//line gititemsummary.qtpl:36
+//line gititemsummary.qtpl:18
+	StreamListTags(qw422016, name, g.Tags)
+//line gititemsummary.qtpl:18
 	qw422016.N().S(`
   </div>
   <div class="col-md-4">
     <div class="event-list">
       `)
-//line gititemsummary.qtpl:40
+//line gititemsummary.qtpl:22
 	for _, b := range g.Branches {
-//line gititemsummary.qtpl:40
+//line gititemsummary.qtpl:22
 		qw422016.N().S(`
       <div class="row event">
           <div class="col-4">
            `)
-//line gititemsummary.qtpl:43
+//line gititemsummary.qtpl:25
 		qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:43
+//line gititemsummary.qtpl:25
 		qw422016.N().S(`
           </div>
           <div class="col-8">
             <div class="float-end">
               <a href="/`)
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:29
 		qw422016.E().S(name)
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:29
 		qw422016.N().S(`/archive/`)
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:29
 		qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:29
 		qw422016.N().S(`.tar.gz">tar.gz</a>
               <a href="/`)
-//line gititemsummary.qtpl:48
+//line gititemsummary.qtpl:30
 		qw422016.E().S(name)
-//line gititemsummary.qtpl:48
+//line gititemsummary.qtpl:30
 		qw422016.N().S(`/tree/`)
-//line gititemsummary.qtpl:48
+//line gititemsummary.qtpl:30
 		qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:48
+//line gititemsummary.qtpl:30
 		qw422016.N().S(`/">tree</a>
               <a href="/`)
-//line gititemsummary.qtpl:49
+//line gititemsummary.qtpl:31
 		qw422016.E().S(name)
-//line gititemsummary.qtpl:49
+//line gititemsummary.qtpl:31
 		qw422016.N().S(`/log/`)
-//line gititemsummary.qtpl:49
+//line gititemsummary.qtpl:31
 		qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:49
+//line gititemsummary.qtpl:31
 		qw422016.N().S(`/">log</a>
             </div>
           </div>
       </div>
       `)
-//line gititemsummary.qtpl:53
+//line gititemsummary.qtpl:35
 	}
-//line gititemsummary.qtpl:53
+//line gititemsummary.qtpl:35
 	qw422016.N().S(`
     </div>
   </div>
@@ -196,48 +136,48 @@ </div>
 <div class="row">
   <div class="event-list">
     `)
-//line gititemsummary.qtpl:59
+//line gititemsummary.qtpl:41
 	for _, c := range g.Commits {
-//line gititemsummary.qtpl:59
+//line gititemsummary.qtpl:41
 		qw422016.N().S(`
     `)
-//line gititemsummary.qtpl:60
+//line gititemsummary.qtpl:42
 		StreamCommit(qw422016, name, c)
-//line gititemsummary.qtpl:60
+//line gititemsummary.qtpl:42
 		qw422016.N().S(`
     `)
-//line gititemsummary.qtpl:61
+//line gititemsummary.qtpl:43
 	}
-//line gititemsummary.qtpl:61
+//line gititemsummary.qtpl:43
 	qw422016.N().S(`
   </div>
 </div>
 `)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 }
 
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 func (g *GitItemSummaryPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	g.StreamGitContent(qw422016, name, ref)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	qt422016.ReleaseWriter(qw422016)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 }
 
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 func (g *GitItemSummaryPage) GitContent(name, ref string) string {
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	qb422016 := qt422016.AcquireByteBuffer()
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	g.WriteGitContent(qb422016, name, ref)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	qs422016 := string(qb422016.B)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	qt422016.ReleaseByteBuffer(qb422016)
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 	return qs422016
-//line gititemsummary.qtpl:64
+//line gititemsummary.qtpl:46
 }
diff --git a/templates/gititemtree.qtpl b/templates/gititemtree.qtpl
index 86fb29cbac5c8aa52d98b729ca13e839ca839db8..5898506af0e41201754aec08dcb72171efd2a675 100644
--- a/templates/gititemtree.qtpl
+++ b/templates/gititemtree.qtpl
@@ -15,7 +15,7 @@ )
 %}
 
 {% code func url(name, mode, ref, filename string, path []string) string {
-    return u.Root().
+    return u.NewPathing().
         AddPath(name).
         AddPath(mode).
         AddPath(ref).
diff --git a/templates/gititemtree.qtpl.go b/templates/gititemtree.qtpl.go
index c0fc3a7787bc21e5a1755a73a878af40c1531c8c..f8d1fd2880caeda1c09bed34c6cdf9e8193bbec0 100644
--- a/templates/gititemtree.qtpl.go
+++ b/templates/gititemtree.qtpl.go
@@ -38,7 +38,7 @@ )
 
 //line gititemtree.qtpl:17
 func url(name, mode, ref, filename string, path []string) string {
-	return u.Root().
+	return u.NewPathing().
 		AddPath(name).
 		AddPath(mode).
 		AddPath(ref).
diff --git a/templates/tags.qtpl b/templates/tags.qtpl
new file mode 100644
index 0000000000000000000000000000000000000000..5cd617fc26b94a2513c0b1fc86d89c9d6e8b8132
--- /dev/null
+++ b/templates/tags.qtpl
@@ -0,0 +1,31 @@
+{% import "git.gabrielgio.me/cerrado/pkg/git" %}
+
+{% func ListTags(name string, tags []*git.TagReference) %}
+{% if len(tags) > 0 %}
+<div class="event-list">
+  {% for _, t := range tags %}
+  <div class="event me-md-2">
+    <div class="row ">
+      <div class="col-4">
+       <a title="{%s t.HashString() %}" href="/{%s name %}/commit/{%s t.HashString() %}">{%s t.ShortName() %}</a>
+      </div>
+      <div class="col-8">
+        <div class="float-end">
+          <a href="/{%s name %}/archive/{%s t.ShortName() %}.tar.gz">tar.gz</a>
+          <a href="/{%s name %}/tree/{%s t.ShortName() %}/">tree</a>
+          <a href="/{%s name %}/log/{%s t.ShortName() %}/">log</a>
+        </div>
+      </div>
+    </div>
+    {% if t.Message() != "" %}
+    <div class="code-view">
+      <pre>{%s t.Message() %}</pre>
+    </div>
+    {% endif %}
+  </div>
+  {% endfor %}
+</div>
+{% else %}
+    <p> No tags </p>
+{% endif %}
+{% endfunc %}
diff --git a/templates/tags.qtpl.go b/templates/tags.qtpl.go
new file mode 100644
index 0000000000000000000000000000000000000000..7d8eca8f5a309f79029bdef9a1ec902ff0a40375
--- /dev/null
+++ b/templates/tags.qtpl.go
@@ -0,0 +1,154 @@
+// Code generated by qtc from "tags.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+//line tags.qtpl:1
+package templates
+
+//line tags.qtpl:1
+import "git.gabrielgio.me/cerrado/pkg/git"
+
+//line tags.qtpl:3
+import (
+	qtio422016 "io"
+
+	qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line tags.qtpl:3
+var (
+	_ = qtio422016.Copy
+	_ = qt422016.AcquireByteBuffer
+)
+
+//line tags.qtpl:3
+func StreamListTags(qw422016 *qt422016.Writer, name string, tags []*git.TagReference) {
+//line tags.qtpl:3
+	qw422016.N().S(`
+`)
+//line tags.qtpl:4
+	if len(tags) > 0 {
+//line tags.qtpl:4
+		qw422016.N().S(`
+<div class="event-list">
+  `)
+//line tags.qtpl:6
+		for _, t := range tags {
+//line tags.qtpl:6
+			qw422016.N().S(`
+  <div class="event me-md-2">
+    <div class="row ">
+      <div class="col-4">
+       <a title="`)
+//line tags.qtpl:10
+			qw422016.E().S(t.HashString())
+//line tags.qtpl:10
+			qw422016.N().S(`" href="/`)
+//line tags.qtpl:10
+			qw422016.E().S(name)
+//line tags.qtpl:10
+			qw422016.N().S(`/commit/`)
+//line tags.qtpl:10
+			qw422016.E().S(t.HashString())
+//line tags.qtpl:10
+			qw422016.N().S(`">`)
+//line tags.qtpl:10
+			qw422016.E().S(t.ShortName())
+//line tags.qtpl:10
+			qw422016.N().S(`</a>
+      </div>
+      <div class="col-8">
+        <div class="float-end">
+          <a href="/`)
+//line tags.qtpl:14
+			qw422016.E().S(name)
+//line tags.qtpl:14
+			qw422016.N().S(`/archive/`)
+//line tags.qtpl:14
+			qw422016.E().S(t.ShortName())
+//line tags.qtpl:14
+			qw422016.N().S(`.tar.gz">tar.gz</a>
+          <a href="/`)
+//line tags.qtpl:15
+			qw422016.E().S(name)
+//line tags.qtpl:15
+			qw422016.N().S(`/tree/`)
+//line tags.qtpl:15
+			qw422016.E().S(t.ShortName())
+//line tags.qtpl:15
+			qw422016.N().S(`/">tree</a>
+          <a href="/`)
+//line tags.qtpl:16
+			qw422016.E().S(name)
+//line tags.qtpl:16
+			qw422016.N().S(`/log/`)
+//line tags.qtpl:16
+			qw422016.E().S(t.ShortName())
+//line tags.qtpl:16
+			qw422016.N().S(`/">log</a>
+        </div>
+      </div>
+    </div>
+    `)
+//line tags.qtpl:20
+			if t.Message() != "" {
+//line tags.qtpl:20
+				qw422016.N().S(`
+    <div class="code-view">
+      <pre>`)
+//line tags.qtpl:22
+				qw422016.E().S(t.Message())
+//line tags.qtpl:22
+				qw422016.N().S(`</pre>
+    </div>
+    `)
+//line tags.qtpl:24
+			}
+//line tags.qtpl:24
+			qw422016.N().S(`
+  </div>
+  `)
+//line tags.qtpl:26
+		}
+//line tags.qtpl:26
+		qw422016.N().S(`
+</div>
+`)
+//line tags.qtpl:28
+	} else {
+//line tags.qtpl:28
+		qw422016.N().S(`
+    <p> No tags </p>
+`)
+//line tags.qtpl:30
+	}
+//line tags.qtpl:30
+	qw422016.N().S(`
+`)
+//line tags.qtpl:31
+}
+
+//line tags.qtpl:31
+func WriteListTags(qq422016 qtio422016.Writer, name string, tags []*git.TagReference) {
+//line tags.qtpl:31
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line tags.qtpl:31
+	StreamListTags(qw422016, name, tags)
+//line tags.qtpl:31
+	qt422016.ReleaseWriter(qw422016)
+//line tags.qtpl:31
+}
+
+//line tags.qtpl:31
+func ListTags(name string, tags []*git.TagReference) string {
+//line tags.qtpl:31
+	qb422016 := qt422016.AcquireByteBuffer()
+//line tags.qtpl:31
+	WriteListTags(qb422016, name, tags)
+//line tags.qtpl:31
+	qs422016 := string(qb422016.B)
+//line tags.qtpl:31
+	qt422016.ReleaseByteBuffer(qb422016)
+//line tags.qtpl:31
+	return qs422016
+//line tags.qtpl:31
+}