cerrado @ 44bc8e4078a09857ad86691a83e7ba7d4e3a69c4

ref: Simplify path builder code
   1diff --git a/README.md b/README.md
   2index e49e6bc371427c1555d6339562a74b0b66f0bf80..bd5e69eb48b984a8f2648b4e5474352d62a015fa 100644
   3--- a/README.md
   4+++ b/README.md
   5@@ -23,8 +23,6 @@ To run the project you just need to do a make run.
   6 
   7 ### TODO
   8 
   9-- Add path to tree view
  10-    - Fix href with extra slash
  11 - Add message to tags
  12 - Add link to tar browser from commit page
  13 - Add patch to the commit page
  14diff --git a/pkg/git/git.go b/pkg/git/git.go
  15index 6b58d35cd3208ba2d12937ad1b31baca0c8ad78b..7341c1bc3f04e6d91f17ac877bb523afd5ee19e2 100644
  16--- a/pkg/git/git.go
  17+++ b/pkg/git/git.go
  18@@ -38,6 +38,11 @@ 		mode    fs.FileMode
  19 		modTime time.Time
  20 		isDir   bool
  21 	}
  22+
  23+	TagReference struct {
  24+		ref *plumbing.Reference
  25+		tag *object.Tag
  26+	}
  27 )
  28 
  29 func OpenRepository(dir string) (*GitRepository, error) {
  30@@ -119,18 +124,31 @@ func (g *GitRepository) Head() (*plumbing.Reference, error) {
  31 	return g.repository.Head()
  32 }
  33 
  34-func (g *GitRepository) Tags() ([]*plumbing.Reference, error) {
  35-	ti, err := g.repository.Tags()
  36+func (g *GitRepository) Tags() ([]*TagReference, error) {
  37+	iter, err := g.repository.Tags()
  38 	if err != nil {
  39 		return nil, err
  40 	}
  41 
  42-	tags := []*plumbing.Reference{}
  43-	err = ti.ForEach(func(t *plumbing.Reference) error {
  44-		tags = append(tags, t)
  45+	tags := make([]*TagReference, 0)
  46+
  47+	if err := iter.ForEach(func(ref *plumbing.Reference) error {
  48+		obj, err := g.repository.TagObject(ref.Hash())
  49+		switch err {
  50+		case nil:
  51+			tags = append(tags, &TagReference{
  52+				ref: ref,
  53+				tag: obj,
  54+			})
  55+		case plumbing.ErrObjectNotFound:
  56+			tags = append(tags, &TagReference{
  57+				ref: ref,
  58+			})
  59+		default:
  60+			return err
  61+		}
  62 		return nil
  63-	})
  64-	if err != nil {
  65+	}); err != nil {
  66 		return nil, err
  67 	}
  68 
  69@@ -361,3 +379,19 @@
  70 func (i *infoWrapper) Sys() any {
  71 	return nil
  72 }
  73+
  74+func (t *TagReference) HashString() string {
  75+	return t.ref.Hash().String()
  76+}
  77+
  78+func (t *TagReference) ShortName() string {
  79+	return t.ref.Name().Short()
  80+}
  81+
  82+func (t *TagReference) Message() string {
  83+	if t.tag != nil {
  84+		return t.tag.Message
  85+	}
  86+	return ""
  87+
  88+}
  89diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
  90index 5e5012263de12db42cb7457a21a2fad99ee50b89..9549f0e414d81baa03f6c1e385ea5c872c0e31f5 100644
  91--- a/pkg/handler/git/handler.go
  92+++ b/pkg/handler/git/handler.go
  93@@ -18,7 +18,6 @@ 	"github.com/alecthomas/chroma/v2"
  94 	"github.com/alecthomas/chroma/v2/formatters/html"
  95 	"github.com/alecthomas/chroma/v2/lexers"
  96 	"github.com/alecthomas/chroma/v2/styles"
  97-	"github.com/go-git/go-git/v5/plumbing"
  98 	"github.com/go-git/go-git/v5/plumbing/object"
  99 	"github.com/gomarkdown/markdown"
 100 	markdownhtml "github.com/gomarkdown/markdown/html"
 101@@ -27,30 +26,16 @@ )
 102 
 103 type (
 104 	GitHandler struct {
 105-		gitService gitService
 106+		gitService *service.GitService
 107 		readmePath string
 108 	}
 109 
 110-	gitService interface {
 111-		ListRepositories() ([]*service.Repository, error)
 112-		ListCommits(name string, ref string, count int) ([]*object.Commit, error)
 113-		LastCommit(name string, ref string) (*object.Commit, error)
 114-		GetHead(name string) (*plumbing.Reference, error)
 115-		GetTree(name, ref, path string) (*object.Tree, error)
 116-		IsBinary(name, ref, path string) (bool, error)
 117-		GetFileContent(name, ref, path string) ([]byte, error)
 118-		GetAbout(name string) ([]byte, error)
 119-		ListTags(name string) ([]*plumbing.Reference, error)
 120-		ListBranches(name string) ([]*plumbing.Reference, error)
 121-		WriteTarGZip(w io.Writer, name, ref, prefix string) error
 122-	}
 123-
 124 	configurationRepository interface {
 125 		GetRootReadme() string
 126 	}
 127 )
 128 
 129-func NewGitHandler(gitService gitService, confRepo configurationRepository) *GitHandler {
 130+func NewGitHandler(gitService *service.GitService, confRepo configurationRepository) *GitHandler {
 131 	return &GitHandler{
 132 		gitService: gitService,
 133 		readmePath: confRepo.GetRootReadme(),
 134diff --git a/pkg/service/git.go b/pkg/service/git.go
 135index df4e3aa36917ce74895247c02a4f2a4f39f868b8..b368f0cc321b64d593bd2399e86c5de8d43c77f7 100644
 136--- a/pkg/service/git.go
 137+++ b/pkg/service/git.go
 138@@ -217,7 +217,7 @@
 139 	return file, nil
 140 }
 141 
 142-func (g *GitService) ListTags(name string) ([]*plumbing.Reference, error) {
 143+func (g *GitService) ListTags(name string) ([]*git.TagReference, error) {
 144 	r := g.configRepo.GetByName(name)
 145 	if r == nil {
 146 		return nil, ErrRepositoryNotFound
 147diff --git a/pkg/u/file.go b/pkg/u/file.go
 148index fafe0fb00f4b30262594af5ab6b32873515301bb..5010b3eef2a8ea0c37b6bbeb23b7ab721197a93c 100644
 149--- a/pkg/u/file.go
 150+++ b/pkg/u/file.go
 151@@ -4,7 +4,7 @@ import (
 152 	"errors"
 153 	"log/slog"
 154 	"os"
 155-	"path/filepath"
 156+	"strings"
 157 )
 158 
 159 func FileExist(filename string) bool {
 160@@ -22,21 +22,43 @@ 	}
 161 }
 162 
 163 // This is just a slin wraper to make easier to compose path in the template
 164-type Pathing string
 165+type Pathing struct {
 166+	sb strings.Builder
 167+}
 168 
 169-func Root() Pathing {
 170-	return "/"
 171+func NewPathing() *Pathing {
 172+	return &Pathing{}
 173 }
 174 
 175-func (s Pathing) AddPath(p string) Pathing {
 176-	return Pathing(filepath.Join(string(s), p))
 177+func (s *Pathing) AddPath(p string) *Pathing {
 178+	if len(p) == 0 {
 179+		return s
 180+	}
 181+
 182+	// if it has trailing / remove it
 183+	if p[len(p)-1] == '/' {
 184+		p = p[:len(p)-1]
 185+		return s.AddPath(p)
 186+	}
 187+
 188+	// if it does not have it so add
 189+	if p[0] == '/' {
 190+		s.sb.WriteString(p)
 191+	} else {
 192+		s.sb.WriteString("/" + p)
 193+	}
 194+
 195+	return s
 196 }
 197 
 198-func (s Pathing) AddPaths(p []string) Pathing {
 199-	f := filepath.Join(p...)
 200-	return Pathing(filepath.Join(string(s), f))
 201+func (s *Pathing) AddPaths(p []string) *Pathing {
 202+	for _, v := range p {
 203+		s.AddPath(v)
 204+	}
 205+
 206+	return s
 207 }
 208 
 209-func (s Pathing) Done() string {
 210-	return string(s)
 211+func (s *Pathing) Done() string {
 212+	return s.sb.String()
 213 }
 214diff --git a/pkg/u/file_test.go b/pkg/u/file_test.go
 215new file mode 100644
 216index 0000000000000000000000000000000000000000..b7d69752f1a0d9d7e6b0e7c828a417e1f8162b15
 217--- /dev/null
 218+++ b/pkg/u/file_test.go
 219@@ -0,0 +1,59 @@
 220+// go:build unit
 221+package u
 222+
 223+import "testing"
 224+
 225+func TestPathing(t *testing.T) {
 226+	testCases := []struct {
 227+		name string
 228+		in   []any
 229+		out  string
 230+	}{
 231+		{
 232+			name: "root",
 233+			in:   []any{},
 234+			out:  "",
 235+		},
 236+		{
 237+			name: "empty",
 238+			in: []any{
 239+				"/",
 240+				[]string{"/", "/"},
 241+				"/",
 242+				[]string{"/"},
 243+			},
 244+			out: "",
 245+		},
 246+		{
 247+			name: "empty",
 248+			in: []any{
 249+				"usr",
 250+				[]string{"/share/", "lib"},
 251+				"/demo",
 252+				[]string{"/out//"},
 253+			},
 254+			out: "/usr/share/lib/demo/out",
 255+		},
 256+	}
 257+
 258+	for _, tc := range testCases {
 259+		t.Run(tc.name, func(t *testing.T) {
 260+			r := NewPathing()
 261+
 262+			for _, v := range tc.in {
 263+				switch s := v.(type) {
 264+				case string:
 265+					r = r.AddPath(s)
 266+				case []string:
 267+					r = r.AddPaths(s)
 268+				}
 269+			}
 270+
 271+			path := r.Done()
 272+			if tc.out != path {
 273+				t.Errorf("String mismatch: wanted %s got %s", tc.out, path)
 274+			}
 275+
 276+		})
 277+	}
 278+}
 279diff --git a/templates/gititemrefs.qtpl b/templates/gititemrefs.qtpl
 280index 624408297c9680822ebf314fd7fb955e823ab606..9f4f74b4c0e7b99f398e79238b3f049b8847b62f 100644
 281--- a/templates/gititemrefs.qtpl
 282+++ b/templates/gititemrefs.qtpl
 283@@ -1,8 +1,9 @@
 284 {% import "github.com/go-git/go-git/v5/plumbing" %}
 285+{% import "git.gabrielgio.me/cerrado/pkg/git" %}
 286 
 287 {% code
 288 type GitItemRefsPage struct {
 289-    Tags []*plumbing.Reference
 290+    Tags []*git.TagReference
 291     Branches []*plumbing.Reference
 292 }
 293 %}
 294@@ -12,26 +13,7 @@
 295 {% func (g *GitItemRefsPage) GitContent(name, ref string) %}
 296 <div class="row">
 297   <div class="col-md-8">
 298-    {% if len(g.Tags) > 0 %}
 299-    <div class="event-list">
 300-      {% for _, t := range g.Tags %}
 301-      <div class="row event me-md-2">
 302-          <div class="col-4">
 303-           {%s t.Name().Short() %}
 304-          </div>
 305-          <div class="col-8">
 306-            <div class="float-end">
 307-              <a href="/{%s name %}/archive/{%s t.Name().Short() %}.tar.gz">tar.gz</a>
 308-              <a href="/{%s name %}/tree/{%s t.Name().Short() %}/">tree</a>
 309-              <a href="/{%s name %}/log/{%s t.Name().Short() %}/">log</a>
 310-            </div>
 311-          </div>
 312-      </div>
 313-      {% endfor %}
 314-    </div>
 315-    {% else %}
 316-        <p> No tags </p>
 317-    {% endif %}
 318+    {%= ListTags(name, g.Tags) %}
 319   </div>
 320   <div class="col-md-4">
 321     <div class="event-list">
 322diff --git a/templates/gititemrefs.qtpl.go b/templates/gititemrefs.qtpl.go
 323index da9bfe757f4d6bc91fb3d53c65dc2cd0faa36074..d54301de94e72454f4fc6070f0f5fa906febf1c9 100644
 324--- a/templates/gititemrefs.qtpl.go
 325+++ b/templates/gititemrefs.qtpl.go
 326@@ -7,214 +7,154 @@
 327 //line gititemrefs.qtpl:1
 328 import "github.com/go-git/go-git/v5/plumbing"
 329 
 330-//line gititemrefs.qtpl:3
 331+//line gititemrefs.qtpl:2
 332+import "git.gabrielgio.me/cerrado/pkg/git"
 333+
 334+//line gititemrefs.qtpl:4
 335 import (
 336 	qtio422016 "io"
 337 
 338 	qt422016 "github.com/valyala/quicktemplate"
 339 )
 340 
 341-//line gititemrefs.qtpl:3
 342+//line gititemrefs.qtpl:4
 343 var (
 344 	_ = qtio422016.Copy
 345 	_ = qt422016.AcquireByteBuffer
 346 )
 347 
 348-//line gititemrefs.qtpl:4
 349+//line gititemrefs.qtpl:5
 350 type GitItemRefsPage struct {
 351-	Tags     []*plumbing.Reference
 352+	Tags     []*git.TagReference
 353 	Branches []*plumbing.Reference
 354 }
 355 
 356-//line gititemrefs.qtpl:10
 357+//line gititemrefs.qtpl:11
 358 func (g *GitItemRefsPage) StreamNav(qw422016 *qt422016.Writer, name, ref string) {
 359-//line gititemrefs.qtpl:10
 360+//line gititemrefs.qtpl:11
 361 	StreamGitItemNav(qw422016, name, ref, Refs)
 362-//line gititemrefs.qtpl:10
 363+//line gititemrefs.qtpl:11
 364 }
 365 
 366-//line gititemrefs.qtpl:10
 367+//line gititemrefs.qtpl:11
 368 func (g *GitItemRefsPage) WriteNav(qq422016 qtio422016.Writer, name, ref string) {
 369-//line gititemrefs.qtpl:10
 370+//line gititemrefs.qtpl:11
 371 	qw422016 := qt422016.AcquireWriter(qq422016)
 372-//line gititemrefs.qtpl:10
 373+//line gititemrefs.qtpl:11
 374 	g.StreamNav(qw422016, name, ref)
 375-//line gititemrefs.qtpl:10
 376+//line gititemrefs.qtpl:11
 377 	qt422016.ReleaseWriter(qw422016)
 378-//line gititemrefs.qtpl:10
 379+//line gititemrefs.qtpl:11
 380 }
 381 
 382-//line gititemrefs.qtpl:10
 383+//line gititemrefs.qtpl:11
 384 func (g *GitItemRefsPage) Nav(name, ref string) string {
 385-//line gititemrefs.qtpl:10
 386+//line gititemrefs.qtpl:11
 387 	qb422016 := qt422016.AcquireByteBuffer()
 388-//line gititemrefs.qtpl:10
 389+//line gititemrefs.qtpl:11
 390 	g.WriteNav(qb422016, name, ref)
 391-//line gititemrefs.qtpl:10
 392+//line gititemrefs.qtpl:11
 393 	qs422016 := string(qb422016.B)
 394-//line gititemrefs.qtpl:10
 395+//line gititemrefs.qtpl:11
 396 	qt422016.ReleaseByteBuffer(qb422016)
 397-//line gititemrefs.qtpl:10
 398+//line gititemrefs.qtpl:11
 399 	return qs422016
 400-//line gititemrefs.qtpl:10
 401+//line gititemrefs.qtpl:11
 402 }
 403 
 404-//line gititemrefs.qtpl:12
 405+//line gititemrefs.qtpl:13
 406 func (g *GitItemRefsPage) StreamGitContent(qw422016 *qt422016.Writer, name, ref string) {
 407-//line gititemrefs.qtpl:12
 408+//line gititemrefs.qtpl:13
 409 	qw422016.N().S(`
 410 <div class="row">
 411   <div class="col-md-8">
 412     `)
 413-//line gititemrefs.qtpl:15
 414-	if len(g.Tags) > 0 {
 415-//line gititemrefs.qtpl:15
 416-		qw422016.N().S(`
 417-    <div class="event-list">
 418-      `)
 419-//line gititemrefs.qtpl:17
 420-		for _, t := range g.Tags {
 421-//line gititemrefs.qtpl:17
 422-			qw422016.N().S(`
 423-      <div class="row event me-md-2">
 424-          <div class="col-4">
 425-           `)
 426-//line gititemrefs.qtpl:20
 427-			qw422016.E().S(t.Name().Short())
 428-//line gititemrefs.qtpl:20
 429-			qw422016.N().S(`
 430-          </div>
 431-          <div class="col-8">
 432-            <div class="float-end">
 433-              <a href="/`)
 434-//line gititemrefs.qtpl:24
 435-			qw422016.E().S(name)
 436-//line gititemrefs.qtpl:24
 437-			qw422016.N().S(`/archive/`)
 438-//line gititemrefs.qtpl:24
 439-			qw422016.E().S(t.Name().Short())
 440-//line gititemrefs.qtpl:24
 441-			qw422016.N().S(`.tar.gz">tar.gz</a>
 442-              <a href="/`)
 443-//line gititemrefs.qtpl:25
 444-			qw422016.E().S(name)
 445-//line gititemrefs.qtpl:25
 446-			qw422016.N().S(`/tree/`)
 447-//line gititemrefs.qtpl:25
 448-			qw422016.E().S(t.Name().Short())
 449-//line gititemrefs.qtpl:25
 450-			qw422016.N().S(`/">tree</a>
 451-              <a href="/`)
 452-//line gititemrefs.qtpl:26
 453-			qw422016.E().S(name)
 454-//line gititemrefs.qtpl:26
 455-			qw422016.N().S(`/log/`)
 456-//line gititemrefs.qtpl:26
 457-			qw422016.E().S(t.Name().Short())
 458-//line gititemrefs.qtpl:26
 459-			qw422016.N().S(`/">log</a>
 460-            </div>
 461-          </div>
 462-      </div>
 463-      `)
 464-//line gititemrefs.qtpl:30
 465-		}
 466-//line gititemrefs.qtpl:30
 467-		qw422016.N().S(`
 468-    </div>
 469-    `)
 470-//line gititemrefs.qtpl:32
 471-	} else {
 472-//line gititemrefs.qtpl:32
 473-		qw422016.N().S(`
 474-        <p> No tags </p>
 475-    `)
 476-//line gititemrefs.qtpl:34
 477-	}
 478-//line gititemrefs.qtpl:34
 479+//line gititemrefs.qtpl:16
 480+	StreamListTags(qw422016, name, g.Tags)
 481+//line gititemrefs.qtpl:16
 482 	qw422016.N().S(`
 483   </div>
 484   <div class="col-md-4">
 485     <div class="event-list">
 486       `)
 487-//line gititemrefs.qtpl:38
 488+//line gititemrefs.qtpl:20
 489 	for _, b := range g.Branches {
 490-//line gititemrefs.qtpl:38
 491+//line gititemrefs.qtpl:20
 492 		qw422016.N().S(`
 493       <div class="row event">
 494           <div class="col-4">
 495            `)
 496-//line gititemrefs.qtpl:41
 497+//line gititemrefs.qtpl:23
 498 		qw422016.E().S(b.Name().Short())
 499-//line gititemrefs.qtpl:41
 500+//line gititemrefs.qtpl:23
 501 		qw422016.N().S(`
 502           </div>
 503           <div class="col-8">
 504             <div class="float-end">
 505               <a href="/`)
 506-//line gititemrefs.qtpl:45
 507+//line gititemrefs.qtpl:27
 508 		qw422016.E().S(name)
 509-//line gititemrefs.qtpl:45
 510+//line gititemrefs.qtpl:27
 511 		qw422016.N().S(`/archive/`)
 512-//line gititemrefs.qtpl:45
 513+//line gititemrefs.qtpl:27
 514 		qw422016.E().S(b.Name().Short())
 515-//line gititemrefs.qtpl:45
 516+//line gititemrefs.qtpl:27
 517 		qw422016.N().S(`.tar.gz">tar.gz</a>
 518               <a href="/`)
 519-//line gititemrefs.qtpl:46
 520+//line gititemrefs.qtpl:28
 521 		qw422016.E().S(name)
 522-//line gititemrefs.qtpl:46
 523+//line gititemrefs.qtpl:28
 524 		qw422016.N().S(`/tree/`)
 525-//line gititemrefs.qtpl:46
 526+//line gititemrefs.qtpl:28
 527 		qw422016.E().S(b.Name().Short())
 528-//line gititemrefs.qtpl:46
 529+//line gititemrefs.qtpl:28
 530 		qw422016.N().S(`/">tree</a>
 531               <a href="/`)
 532-//line gititemrefs.qtpl:47
 533+//line gititemrefs.qtpl:29
 534 		qw422016.E().S(name)
 535-//line gititemrefs.qtpl:47
 536+//line gititemrefs.qtpl:29
 537 		qw422016.N().S(`/log/`)
 538-//line gititemrefs.qtpl:47
 539+//line gititemrefs.qtpl:29
 540 		qw422016.E().S(b.Name().Short())
 541-//line gititemrefs.qtpl:47
 542+//line gititemrefs.qtpl:29
 543 		qw422016.N().S(`/">log</a>
 544             </div>
 545           </div>
 546       </div>
 547       `)
 548-//line gititemrefs.qtpl:51
 549+//line gititemrefs.qtpl:33
 550 	}
 551-//line gititemrefs.qtpl:51
 552+//line gititemrefs.qtpl:33
 553 	qw422016.N().S(`
 554     </div>
 555   </div>
 556 </div>
 557 `)
 558-//line gititemrefs.qtpl:55
 559+//line gititemrefs.qtpl:37
 560 }
 561 
 562-//line gititemrefs.qtpl:55
 563+//line gititemrefs.qtpl:37
 564 func (g *GitItemRefsPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) {
 565-//line gititemrefs.qtpl:55
 566+//line gititemrefs.qtpl:37
 567 	qw422016 := qt422016.AcquireWriter(qq422016)
 568-//line gititemrefs.qtpl:55
 569+//line gititemrefs.qtpl:37
 570 	g.StreamGitContent(qw422016, name, ref)
 571-//line gititemrefs.qtpl:55
 572+//line gititemrefs.qtpl:37
 573 	qt422016.ReleaseWriter(qw422016)
 574-//line gititemrefs.qtpl:55
 575+//line gititemrefs.qtpl:37
 576 }
 577 
 578-//line gititemrefs.qtpl:55
 579+//line gititemrefs.qtpl:37
 580 func (g *GitItemRefsPage) GitContent(name, ref string) string {
 581-//line gititemrefs.qtpl:55
 582+//line gititemrefs.qtpl:37
 583 	qb422016 := qt422016.AcquireByteBuffer()
 584-//line gititemrefs.qtpl:55
 585+//line gititemrefs.qtpl:37
 586 	g.WriteGitContent(qb422016, name, ref)
 587-//line gititemrefs.qtpl:55
 588+//line gititemrefs.qtpl:37
 589 	qs422016 := string(qb422016.B)
 590-//line gititemrefs.qtpl:55
 591+//line gititemrefs.qtpl:37
 592 	qt422016.ReleaseByteBuffer(qb422016)
 593-//line gititemrefs.qtpl:55
 594+//line gititemrefs.qtpl:37
 595 	return qs422016
 596-//line gititemrefs.qtpl:55
 597+//line gititemrefs.qtpl:37
 598 }
 599diff --git a/templates/gititemsummary.qtpl b/templates/gititemsummary.qtpl
 600index ef2c534df5b1d5ab85760c7de436b83d5f041e7c..44e160474b48574d3c93773fa3780a17529aa480 100644
 601--- a/templates/gititemsummary.qtpl
 602+++ b/templates/gititemsummary.qtpl
 603@@ -1,9 +1,10 @@
 604 {% import "github.com/go-git/go-git/v5/plumbing" %}
 605 {% import "github.com/go-git/go-git/v5/plumbing/object" %}
 606+{% import "git.gabrielgio.me/cerrado/pkg/git" %}
 607 
 608 {% code
 609 type GitItemSummaryPage struct {
 610-    Tags []*plumbing.Reference
 611+    Tags []*git.TagReference
 612     Branches []*plumbing.Reference
 613     Commits []*object.Commit
 614 }
 615@@ -14,26 +15,7 @@
 616 {% func (g *GitItemSummaryPage) GitContent(name, ref string) %}
 617 <div class="row">
 618   <div class="col-md-8">
 619-    {% if len(g.Tags) > 0 %}
 620-    <div class="event-list">
 621-      {% for _, t := range g.Tags %}
 622-      <div class="row event me-md-2">
 623-          <div class="col-4">
 624-           {%s t.Name().Short() %}
 625-          </div>
 626-          <div class="col-8">
 627-            <div class="float-end">
 628-              <a href="/{%s name %}/archive/{%s t.Name().Short() %}.tar.gz">tar.gz</a>
 629-              <a href="/{%s name %}/tree/{%s t.Name().Short() %}/">tree</a>
 630-              <a href="/{%s name %}/log/{%s t.Name().Short() %}/">log</a>
 631-            </div>
 632-          </div>
 633-      </div>
 634-      {% endfor %}
 635-    </div>
 636-    {% else %}
 637-        <p> No tags </p>
 638-    {% endif %}
 639+    {%= ListTags(name, g.Tags) %}
 640   </div>
 641   <div class="col-md-4">
 642     <div class="event-list">
 643diff --git a/templates/gititemsummary.qtpl.go b/templates/gititemsummary.qtpl.go
 644index 570a95501376383761ed6eeaf38d4e7d25c765ad..24fed9df74108f464aa3b1685e1fa597559620fc 100644
 645--- a/templates/gititemsummary.qtpl.go
 646+++ b/templates/gititemsummary.qtpl.go
 647@@ -10,185 +10,125 @@
 648 //line gititemsummary.qtpl:2
 649 import "github.com/go-git/go-git/v5/plumbing/object"
 650 
 651-//line gititemsummary.qtpl:4
 652+//line gititemsummary.qtpl:3
 653+import "git.gabrielgio.me/cerrado/pkg/git"
 654+
 655+//line gititemsummary.qtpl:5
 656 import (
 657 	qtio422016 "io"
 658 
 659 	qt422016 "github.com/valyala/quicktemplate"
 660 )
 661 
 662-//line gititemsummary.qtpl:4
 663+//line gititemsummary.qtpl:5
 664 var (
 665 	_ = qtio422016.Copy
 666 	_ = qt422016.AcquireByteBuffer
 667 )
 668 
 669-//line gititemsummary.qtpl:5
 670+//line gititemsummary.qtpl:6
 671 type GitItemSummaryPage struct {
 672-	Tags     []*plumbing.Reference
 673+	Tags     []*git.TagReference
 674 	Branches []*plumbing.Reference
 675 	Commits  []*object.Commit
 676 }
 677 
 678-//line gititemsummary.qtpl:12
 679+//line gititemsummary.qtpl:13
 680 func (g *GitItemSummaryPage) StreamNav(qw422016 *qt422016.Writer, name, ref string) {
 681-//line gititemsummary.qtpl:12
 682+//line gititemsummary.qtpl:13
 683 	StreamGitItemNav(qw422016, name, ref, Summary)
 684-//line gititemsummary.qtpl:12
 685+//line gititemsummary.qtpl:13
 686 }
 687 
 688-//line gititemsummary.qtpl:12
 689+//line gititemsummary.qtpl:13
 690 func (g *GitItemSummaryPage) WriteNav(qq422016 qtio422016.Writer, name, ref string) {
 691-//line gititemsummary.qtpl:12
 692+//line gititemsummary.qtpl:13
 693 	qw422016 := qt422016.AcquireWriter(qq422016)
 694-//line gititemsummary.qtpl:12
 695+//line gititemsummary.qtpl:13
 696 	g.StreamNav(qw422016, name, ref)
 697-//line gititemsummary.qtpl:12
 698+//line gititemsummary.qtpl:13
 699 	qt422016.ReleaseWriter(qw422016)
 700-//line gititemsummary.qtpl:12
 701+//line gititemsummary.qtpl:13
 702 }
 703 
 704-//line gititemsummary.qtpl:12
 705+//line gititemsummary.qtpl:13
 706 func (g *GitItemSummaryPage) Nav(name, ref string) string {
 707-//line gititemsummary.qtpl:12
 708+//line gititemsummary.qtpl:13
 709 	qb422016 := qt422016.AcquireByteBuffer()
 710-//line gititemsummary.qtpl:12
 711+//line gititemsummary.qtpl:13
 712 	g.WriteNav(qb422016, name, ref)
 713-//line gititemsummary.qtpl:12
 714+//line gititemsummary.qtpl:13
 715 	qs422016 := string(qb422016.B)
 716-//line gititemsummary.qtpl:12
 717+//line gititemsummary.qtpl:13
 718 	qt422016.ReleaseByteBuffer(qb422016)
 719-//line gititemsummary.qtpl:12
 720+//line gititemsummary.qtpl:13
 721 	return qs422016
 722-//line gititemsummary.qtpl:12
 723+//line gititemsummary.qtpl:13
 724 }
 725 
 726-//line gititemsummary.qtpl:14
 727+//line gititemsummary.qtpl:15
 728 func (g *GitItemSummaryPage) StreamGitContent(qw422016 *qt422016.Writer, name, ref string) {
 729-//line gititemsummary.qtpl:14
 730+//line gititemsummary.qtpl:15
 731 	qw422016.N().S(`
 732 <div class="row">
 733   <div class="col-md-8">
 734     `)
 735-//line gititemsummary.qtpl:17
 736-	if len(g.Tags) > 0 {
 737-//line gititemsummary.qtpl:17
 738-		qw422016.N().S(`
 739-    <div class="event-list">
 740-      `)
 741-//line gititemsummary.qtpl:19
 742-		for _, t := range g.Tags {
 743-//line gititemsummary.qtpl:19
 744-			qw422016.N().S(`
 745-      <div class="row event me-md-2">
 746-          <div class="col-4">
 747-           `)
 748-//line gititemsummary.qtpl:22
 749-			qw422016.E().S(t.Name().Short())
 750-//line gititemsummary.qtpl:22
 751-			qw422016.N().S(`
 752-          </div>
 753-          <div class="col-8">
 754-            <div class="float-end">
 755-              <a href="/`)
 756-//line gititemsummary.qtpl:26
 757-			qw422016.E().S(name)
 758-//line gititemsummary.qtpl:26
 759-			qw422016.N().S(`/archive/`)
 760-//line gititemsummary.qtpl:26
 761-			qw422016.E().S(t.Name().Short())
 762-//line gititemsummary.qtpl:26
 763-			qw422016.N().S(`.tar.gz">tar.gz</a>
 764-              <a href="/`)
 765-//line gititemsummary.qtpl:27
 766-			qw422016.E().S(name)
 767-//line gititemsummary.qtpl:27
 768-			qw422016.N().S(`/tree/`)
 769-//line gititemsummary.qtpl:27
 770-			qw422016.E().S(t.Name().Short())
 771-//line gititemsummary.qtpl:27
 772-			qw422016.N().S(`/">tree</a>
 773-              <a href="/`)
 774-//line gititemsummary.qtpl:28
 775-			qw422016.E().S(name)
 776-//line gititemsummary.qtpl:28
 777-			qw422016.N().S(`/log/`)
 778-//line gititemsummary.qtpl:28
 779-			qw422016.E().S(t.Name().Short())
 780-//line gititemsummary.qtpl:28
 781-			qw422016.N().S(`/">log</a>
 782-            </div>
 783-          </div>
 784-      </div>
 785-      `)
 786-//line gititemsummary.qtpl:32
 787-		}
 788-//line gititemsummary.qtpl:32
 789-		qw422016.N().S(`
 790-    </div>
 791-    `)
 792-//line gititemsummary.qtpl:34
 793-	} else {
 794-//line gititemsummary.qtpl:34
 795-		qw422016.N().S(`
 796-        <p> No tags </p>
 797-    `)
 798-//line gititemsummary.qtpl:36
 799-	}
 800-//line gititemsummary.qtpl:36
 801+//line gititemsummary.qtpl:18
 802+	StreamListTags(qw422016, name, g.Tags)
 803+//line gititemsummary.qtpl:18
 804 	qw422016.N().S(`
 805   </div>
 806   <div class="col-md-4">
 807     <div class="event-list">
 808       `)
 809-//line gititemsummary.qtpl:40
 810+//line gititemsummary.qtpl:22
 811 	for _, b := range g.Branches {
 812-//line gititemsummary.qtpl:40
 813+//line gititemsummary.qtpl:22
 814 		qw422016.N().S(`
 815       <div class="row event">
 816           <div class="col-4">
 817            `)
 818-//line gititemsummary.qtpl:43
 819+//line gititemsummary.qtpl:25
 820 		qw422016.E().S(b.Name().Short())
 821-//line gititemsummary.qtpl:43
 822+//line gititemsummary.qtpl:25
 823 		qw422016.N().S(`
 824           </div>
 825           <div class="col-8">
 826             <div class="float-end">
 827               <a href="/`)
 828-//line gititemsummary.qtpl:47
 829+//line gititemsummary.qtpl:29
 830 		qw422016.E().S(name)
 831-//line gititemsummary.qtpl:47
 832+//line gititemsummary.qtpl:29
 833 		qw422016.N().S(`/archive/`)
 834-//line gititemsummary.qtpl:47
 835+//line gititemsummary.qtpl:29
 836 		qw422016.E().S(b.Name().Short())
 837-//line gititemsummary.qtpl:47
 838+//line gititemsummary.qtpl:29
 839 		qw422016.N().S(`.tar.gz">tar.gz</a>
 840               <a href="/`)
 841-//line gititemsummary.qtpl:48
 842+//line gititemsummary.qtpl:30
 843 		qw422016.E().S(name)
 844-//line gititemsummary.qtpl:48
 845+//line gititemsummary.qtpl:30
 846 		qw422016.N().S(`/tree/`)
 847-//line gititemsummary.qtpl:48
 848+//line gititemsummary.qtpl:30
 849 		qw422016.E().S(b.Name().Short())
 850-//line gititemsummary.qtpl:48
 851+//line gititemsummary.qtpl:30
 852 		qw422016.N().S(`/">tree</a>
 853               <a href="/`)
 854-//line gititemsummary.qtpl:49
 855+//line gititemsummary.qtpl:31
 856 		qw422016.E().S(name)
 857-//line gititemsummary.qtpl:49
 858+//line gititemsummary.qtpl:31
 859 		qw422016.N().S(`/log/`)
 860-//line gititemsummary.qtpl:49
 861+//line gititemsummary.qtpl:31
 862 		qw422016.E().S(b.Name().Short())
 863-//line gititemsummary.qtpl:49
 864+//line gititemsummary.qtpl:31
 865 		qw422016.N().S(`/">log</a>
 866             </div>
 867           </div>
 868       </div>
 869       `)
 870-//line gititemsummary.qtpl:53
 871+//line gititemsummary.qtpl:35
 872 	}
 873-//line gititemsummary.qtpl:53
 874+//line gititemsummary.qtpl:35
 875 	qw422016.N().S(`
 876     </div>
 877   </div>
 878@@ -196,48 +136,48 @@ </div>
 879 <div class="row">
 880   <div class="event-list">
 881     `)
 882-//line gititemsummary.qtpl:59
 883+//line gititemsummary.qtpl:41
 884 	for _, c := range g.Commits {
 885-//line gititemsummary.qtpl:59
 886+//line gititemsummary.qtpl:41
 887 		qw422016.N().S(`
 888     `)
 889-//line gititemsummary.qtpl:60
 890+//line gititemsummary.qtpl:42
 891 		StreamCommit(qw422016, name, c)
 892-//line gititemsummary.qtpl:60
 893+//line gititemsummary.qtpl:42
 894 		qw422016.N().S(`
 895     `)
 896-//line gititemsummary.qtpl:61
 897+//line gititemsummary.qtpl:43
 898 	}
 899-//line gititemsummary.qtpl:61
 900+//line gititemsummary.qtpl:43
 901 	qw422016.N().S(`
 902   </div>
 903 </div>
 904 `)
 905-//line gititemsummary.qtpl:64
 906+//line gititemsummary.qtpl:46
 907 }
 908 
 909-//line gititemsummary.qtpl:64
 910+//line gititemsummary.qtpl:46
 911 func (g *GitItemSummaryPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) {
 912-//line gititemsummary.qtpl:64
 913+//line gititemsummary.qtpl:46
 914 	qw422016 := qt422016.AcquireWriter(qq422016)
 915-//line gititemsummary.qtpl:64
 916+//line gititemsummary.qtpl:46
 917 	g.StreamGitContent(qw422016, name, ref)
 918-//line gititemsummary.qtpl:64
 919+//line gititemsummary.qtpl:46
 920 	qt422016.ReleaseWriter(qw422016)
 921-//line gititemsummary.qtpl:64
 922+//line gititemsummary.qtpl:46
 923 }
 924 
 925-//line gititemsummary.qtpl:64
 926+//line gititemsummary.qtpl:46
 927 func (g *GitItemSummaryPage) GitContent(name, ref string) string {
 928-//line gititemsummary.qtpl:64
 929+//line gititemsummary.qtpl:46
 930 	qb422016 := qt422016.AcquireByteBuffer()
 931-//line gititemsummary.qtpl:64
 932+//line gititemsummary.qtpl:46
 933 	g.WriteGitContent(qb422016, name, ref)
 934-//line gititemsummary.qtpl:64
 935+//line gititemsummary.qtpl:46
 936 	qs422016 := string(qb422016.B)
 937-//line gititemsummary.qtpl:64
 938+//line gititemsummary.qtpl:46
 939 	qt422016.ReleaseByteBuffer(qb422016)
 940-//line gititemsummary.qtpl:64
 941+//line gititemsummary.qtpl:46
 942 	return qs422016
 943-//line gititemsummary.qtpl:64
 944+//line gititemsummary.qtpl:46
 945 }
 946diff --git a/templates/gititemtree.qtpl b/templates/gititemtree.qtpl
 947index 86fb29cbac5c8aa52d98b729ca13e839ca839db8..5898506af0e41201754aec08dcb72171efd2a675 100644
 948--- a/templates/gititemtree.qtpl
 949+++ b/templates/gititemtree.qtpl
 950@@ -15,7 +15,7 @@ )
 951 %}
 952 
 953 {% code func url(name, mode, ref, filename string, path []string) string {
 954-    return u.Root().
 955+    return u.NewPathing().
 956         AddPath(name).
 957         AddPath(mode).
 958         AddPath(ref).
 959diff --git a/templates/gititemtree.qtpl.go b/templates/gititemtree.qtpl.go
 960index c0fc3a7787bc21e5a1755a73a878af40c1531c8c..f8d1fd2880caeda1c09bed34c6cdf9e8193bbec0 100644
 961--- a/templates/gititemtree.qtpl.go
 962+++ b/templates/gititemtree.qtpl.go
 963@@ -38,7 +38,7 @@ )
 964 
 965 //line gititemtree.qtpl:17
 966 func url(name, mode, ref, filename string, path []string) string {
 967-	return u.Root().
 968+	return u.NewPathing().
 969 		AddPath(name).
 970 		AddPath(mode).
 971 		AddPath(ref).
 972diff --git a/templates/tags.qtpl b/templates/tags.qtpl
 973new file mode 100644
 974index 0000000000000000000000000000000000000000..5cd617fc26b94a2513c0b1fc86d89c9d6e8b8132
 975--- /dev/null
 976+++ b/templates/tags.qtpl
 977@@ -0,0 +1,31 @@
 978+{% import "git.gabrielgio.me/cerrado/pkg/git" %}
 979+
 980+{% func ListTags(name string, tags []*git.TagReference) %}
 981+{% if len(tags) > 0 %}
 982+<div class="event-list">
 983+  {% for _, t := range tags %}
 984+  <div class="event me-md-2">
 985+    <div class="row ">
 986+      <div class="col-4">
 987+       <a title="{%s t.HashString() %}" href="/{%s name %}/commit/{%s t.HashString() %}">{%s t.ShortName() %}</a>
 988+      </div>
 989+      <div class="col-8">
 990+        <div class="float-end">
 991+          <a href="/{%s name %}/archive/{%s t.ShortName() %}.tar.gz">tar.gz</a>
 992+          <a href="/{%s name %}/tree/{%s t.ShortName() %}/">tree</a>
 993+          <a href="/{%s name %}/log/{%s t.ShortName() %}/">log</a>
 994+        </div>
 995+      </div>
 996+    </div>
 997+    {% if t.Message() != "" %}
 998+    <div class="code-view">
 999+      <pre>{%s t.Message() %}</pre>
1000+    </div>
1001+    {% endif %}
1002+  </div>
1003+  {% endfor %}
1004+</div>
1005+{% else %}
1006+    <p> No tags </p>
1007+{% endif %}
1008+{% endfunc %}
1009diff --git a/templates/tags.qtpl.go b/templates/tags.qtpl.go
1010new file mode 100644
1011index 0000000000000000000000000000000000000000..7d8eca8f5a309f79029bdef9a1ec902ff0a40375
1012--- /dev/null
1013+++ b/templates/tags.qtpl.go
1014@@ -0,0 +1,154 @@
1015+// Code generated by qtc from "tags.qtpl". DO NOT EDIT.
1016+// See https://github.com/valyala/quicktemplate for details.
1017+
1018+//line tags.qtpl:1
1019+package templates
1020+
1021+//line tags.qtpl:1
1022+import "git.gabrielgio.me/cerrado/pkg/git"
1023+
1024+//line tags.qtpl:3
1025+import (
1026+	qtio422016 "io"
1027+
1028+	qt422016 "github.com/valyala/quicktemplate"
1029+)
1030+
1031+//line tags.qtpl:3
1032+var (
1033+	_ = qtio422016.Copy
1034+	_ = qt422016.AcquireByteBuffer
1035+)
1036+
1037+//line tags.qtpl:3
1038+func StreamListTags(qw422016 *qt422016.Writer, name string, tags []*git.TagReference) {
1039+//line tags.qtpl:3
1040+	qw422016.N().S(`
1041+`)
1042+//line tags.qtpl:4
1043+	if len(tags) > 0 {
1044+//line tags.qtpl:4
1045+		qw422016.N().S(`
1046+<div class="event-list">
1047+  `)
1048+//line tags.qtpl:6
1049+		for _, t := range tags {
1050+//line tags.qtpl:6
1051+			qw422016.N().S(`
1052+  <div class="event me-md-2">
1053+    <div class="row ">
1054+      <div class="col-4">
1055+       <a title="`)
1056+//line tags.qtpl:10
1057+			qw422016.E().S(t.HashString())
1058+//line tags.qtpl:10
1059+			qw422016.N().S(`" href="/`)
1060+//line tags.qtpl:10
1061+			qw422016.E().S(name)
1062+//line tags.qtpl:10
1063+			qw422016.N().S(`/commit/`)
1064+//line tags.qtpl:10
1065+			qw422016.E().S(t.HashString())
1066+//line tags.qtpl:10
1067+			qw422016.N().S(`">`)
1068+//line tags.qtpl:10
1069+			qw422016.E().S(t.ShortName())
1070+//line tags.qtpl:10
1071+			qw422016.N().S(`</a>
1072+      </div>
1073+      <div class="col-8">
1074+        <div class="float-end">
1075+          <a href="/`)
1076+//line tags.qtpl:14
1077+			qw422016.E().S(name)
1078+//line tags.qtpl:14
1079+			qw422016.N().S(`/archive/`)
1080+//line tags.qtpl:14
1081+			qw422016.E().S(t.ShortName())
1082+//line tags.qtpl:14
1083+			qw422016.N().S(`.tar.gz">tar.gz</a>
1084+          <a href="/`)
1085+//line tags.qtpl:15
1086+			qw422016.E().S(name)
1087+//line tags.qtpl:15
1088+			qw422016.N().S(`/tree/`)
1089+//line tags.qtpl:15
1090+			qw422016.E().S(t.ShortName())
1091+//line tags.qtpl:15
1092+			qw422016.N().S(`/">tree</a>
1093+          <a href="/`)
1094+//line tags.qtpl:16
1095+			qw422016.E().S(name)
1096+//line tags.qtpl:16
1097+			qw422016.N().S(`/log/`)
1098+//line tags.qtpl:16
1099+			qw422016.E().S(t.ShortName())
1100+//line tags.qtpl:16
1101+			qw422016.N().S(`/">log</a>
1102+        </div>
1103+      </div>
1104+    </div>
1105+    `)
1106+//line tags.qtpl:20
1107+			if t.Message() != "" {
1108+//line tags.qtpl:20
1109+				qw422016.N().S(`
1110+    <div class="code-view">
1111+      <pre>`)
1112+//line tags.qtpl:22
1113+				qw422016.E().S(t.Message())
1114+//line tags.qtpl:22
1115+				qw422016.N().S(`</pre>
1116+    </div>
1117+    `)
1118+//line tags.qtpl:24
1119+			}
1120+//line tags.qtpl:24
1121+			qw422016.N().S(`
1122+  </div>
1123+  `)
1124+//line tags.qtpl:26
1125+		}
1126+//line tags.qtpl:26
1127+		qw422016.N().S(`
1128+</div>
1129+`)
1130+//line tags.qtpl:28
1131+	} else {
1132+//line tags.qtpl:28
1133+		qw422016.N().S(`
1134+    <p> No tags </p>
1135+`)
1136+//line tags.qtpl:30
1137+	}
1138+//line tags.qtpl:30
1139+	qw422016.N().S(`
1140+`)
1141+//line tags.qtpl:31
1142+}
1143+
1144+//line tags.qtpl:31
1145+func WriteListTags(qq422016 qtio422016.Writer, name string, tags []*git.TagReference) {
1146+//line tags.qtpl:31
1147+	qw422016 := qt422016.AcquireWriter(qq422016)
1148+//line tags.qtpl:31
1149+	StreamListTags(qw422016, name, tags)
1150+//line tags.qtpl:31
1151+	qt422016.ReleaseWriter(qw422016)
1152+//line tags.qtpl:31
1153+}
1154+
1155+//line tags.qtpl:31
1156+func ListTags(name string, tags []*git.TagReference) string {
1157+//line tags.qtpl:31
1158+	qb422016 := qt422016.AcquireByteBuffer()
1159+//line tags.qtpl:31
1160+	WriteListTags(qb422016, name, tags)
1161+//line tags.qtpl:31
1162+	qs422016 := string(qb422016.B)
1163+//line tags.qtpl:31
1164+	qt422016.ReleaseByteBuffer(qb422016)
1165+//line tags.qtpl:31
1166+	return qs422016
1167+//line tags.qtpl:31
1168+}