cerrado @ 18aa098f50e2a2c7db01dd4d04dde460fd40f5d5

feat: Add blob endpoint
diff --git a/pkg/git/git.go b/pkg/git/git.go
index ce72465a84f2b8f0c65411b176b7a075ca031609..6a7b91fcba4060e8c2462a6ff03cf9fd309218d4 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -13,7 +13,8 @@
 var ()
 
 var (
-	MissingRefErr = errors.New("Reference not found")
+	MissingRefErr  = errors.New("Reference not found")
+	TreeForFileErr = errors.New("Trying to get tree of a file")
 )
 
 type (
@@ -145,9 +146,73 @@
 	return branches, nil
 }
 
+func (g *GitRepository) Tree(path string) (*object.Tree, error) {
+	err := g.validateRef()
+	if err != nil {
+		return nil, err
+	}
+
+	c, err := g.repository.CommitObject(g.ref)
+	if err != nil {
+		return nil, err
+	}
+
+	tree, err := c.Tree()
+	if err != nil {
+		return nil, err
+	}
+
+	if path == "" {
+		return tree, nil
+	} else {
+		o, err := tree.FindEntry(path)
+		if err != nil {
+			return nil, err
+		}
+
+		if !o.Mode.IsFile() {
+			subtree, err := tree.Tree(path)
+			if err != nil {
+				return nil, err
+			}
+			return subtree, nil
+		} else {
+			return nil, TreeForFileErr
+		}
+	}
+}
+
 func (g *GitRepository) validateRef() error {
 	if !g.setRef {
 		return g.SetRef("")
 	}
 	return nil
 }
+
+func (g *GitRepository) FileContent(path string) (string, error) {
+	c, err := g.repository.CommitObject(g.ref)
+	if err != nil {
+		return "", err
+	}
+
+	tree, err := c.Tree()
+	if err != nil {
+		return "", err
+	}
+
+	file, err := tree.File(path)
+	if err != nil {
+		return "", err
+	}
+
+	isbin, err := file.IsBinary()
+	if err != nil {
+		return "", err
+	}
+
+	if !isbin {
+		return file.Contents()
+	} else {
+		return "Binary file", nil
+	}
+}
diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
index e2f40422ed212156620190800a9ec6569bc4c11f..f3e74c747e433b33f1b68d13473ef5ab507b5b95 100644
--- a/pkg/handler/git/handler.go
+++ b/pkg/handler/git/handler.go
@@ -1,11 +1,17 @@
 package git
 
 import (
+	"bytes"
 	"log/slog"
 	"net/http"
+	"path/filepath"
 
 	"git.gabrielgio.me/cerrado/pkg/service"
 	"git.gabrielgio.me/cerrado/templates"
+	"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"
 )
@@ -19,6 +25,8 @@ 	gitService interface {
 		ListRepositories() ([]*service.Repository, error)
 		ListCommits(name string, ref string) ([]*object.Commit, error)
 		GetHead(name string) (*plumbing.Reference, error)
+		GetTree(name, ref, path string) (*object.Tree, error)
+		GetFileContent(name, ref, path string) (string, error)
 		ListTags(name string) ([]*object.Tag, error)
 		ListBranches(name string) ([]*plumbing.Reference, error)
 	}
@@ -107,10 +115,64 @@
 func (g *GitHandler) Tree(w http.ResponseWriter, r *http.Request) {
 	name := r.PathValue("name")
 	ref := r.PathValue("ref")
+	rest := r.PathValue("rest")
+
+	tree, err := g.gitService.GetTree(name, ref, rest)
+	if err != nil {
+		slog.Error("Error loading tree", "error", err)
+		return
+	}
+
 	gitList := &templates.GitItemPage{
-		Name:        name,
-		Ref:         ref,
-		GitItemBase: &templates.GitItemTreePage{},
+		Name: name,
+		Ref:  ref,
+		GitItemBase: &templates.GitItemTreePage{
+			CurrentPath: rest,
+			Tree:        tree,
+			Ref:         ref,
+			Name:        name,
+		},
+	}
+	templates.WritePageTemplate(w, gitList)
+}
+
+func (g *GitHandler) Blob(w http.ResponseWriter, r *http.Request) {
+	name := r.PathValue("name")
+	ref := r.PathValue("ref")
+	rest := r.PathValue("rest")
+
+	file, err := g.gitService.GetFileContent(name, ref, rest)
+	if err != nil {
+		slog.Error("Error loading blob", "error", err)
+		return
+	}
+
+	filename := filepath.Base(rest)
+	lexer := GetLexers(filename)
+	style := styles.Get("xcode")
+	formatter := html.New(
+		html.WithLineNumbers(true),
+	)
+	iterator, err := lexer.Tokenise(nil, file)
+	if err != nil {
+		slog.Error("Error tokenise", "error", err)
+		return
+	}
+
+	var code bytes.Buffer
+	err = formatter.Format(&code, style, iterator)
+	if err != nil {
+		slog.Error("Error format", "error", err)
+		return
+	}
+
+	gitList := &templates.GitItemPage{
+		Name: name,
+		Ref:  ref,
+		GitItemBase: &templates.GitItemBlobPage{
+			File:    rest,
+			Content: code.Bytes(),
+		},
 	}
 	templates.WritePageTemplate(w, gitList)
 }
@@ -134,3 +196,16 @@ 		},
 	}
 	templates.WritePageTemplate(w, gitList)
 }
+
+func GetLexers(filename string) chroma.Lexer {
+	if filename == "APKBUILD" {
+		return lexers.Get("sh")
+	}
+
+	lexer := lexers.Get(filename)
+
+	if lexer == nil {
+		lexer = lexers.Get("txt")
+	}
+	return lexer
+}
diff --git a/pkg/handler/router.go b/pkg/handler/router.go
index bdf883ed8a8af090669e285504118bda8d86450a..ed782f75d525c637e539a4e6c07795cc0d8ac8d2 100644
--- a/pkg/handler/router.go
+++ b/pkg/handler/router.go
@@ -35,7 +35,8 @@ 	mux.HandleFunc("/static/{file}", staticHandler)
 	mux.HandleFunc("/{name}/about/{$}", gitHandler.About)
 	mux.HandleFunc("/{name}", gitHandler.Summary)
 	mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs)
-	mux.HandleFunc("/{name}/tree/{ref}", gitHandler.Tree)
+	mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree)
+	mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob)
 	mux.HandleFunc("/{name}/log/{ref}", gitHandler.Log)
 	mux.HandleFunc("/config", configHander)
 	mux.HandleFunc("/about", aboutHandler.About)
diff --git a/pkg/service/git.go b/pkg/service/git.go
index 9bf11f4e79cb77ae692820236f59db9bc9ea0453..f8867850cd8a76933da262156fdbac70134a27ff 100644
--- a/pkg/service/git.go
+++ b/pkg/service/git.go
@@ -89,6 +89,38 @@ 	}
 	return repo.Commits()
 }
 
+func (g *GitService) GetTree(name, ref, path string) (*object.Tree, error) {
+	// TODO: handle nil
+	r := g.configRepo.GetByName(name)
+
+	repo, err := git.OpenRepository(r.Path)
+	if err != nil {
+		return nil, err
+	}
+	err = repo.SetRef(ref)
+	if err != nil {
+		return nil, err
+	}
+
+	return repo.Tree(path)
+}
+
+func (g *GitService) GetFileContent(name, ref, path string) (string, error) {
+	// TODO: handle nil
+	r := g.configRepo.GetByName(name)
+
+	repo, err := git.OpenRepository(r.Path)
+	if err != nil {
+		return "", err
+	}
+	err = repo.SetRef(ref)
+	if err != nil {
+		return "", err
+	}
+
+	return repo.FileContent(path)
+}
+
 func (g *GitService) ListTags(name string) ([]*object.Tag, error) {
 	// TODO: handle nil
 	r := g.configRepo.GetByName(name)
diff --git a/templates/base.qtpl b/templates/base.qtpl
index 16b878014575bb4d5d6dc275b17774b1d63079c3..180b1ab428bc964511451c36569b198be2f5d3ee 100644
--- a/templates/base.qtpl
+++ b/templates/base.qtpl
@@ -30,6 +30,11 @@         return t.Format("2006-01-02")
     }
 %}
 
+{% code func Ignore[T any](v T, _ error) T {
+        return v
+    }
+%}
+
 Page prints a page implementing Page interface.
 {% func PageTemplate(p Page) %}
 <html lang="en">
diff --git a/templates/base.qtpl.go b/templates/base.qtpl.go
index 6d4d0a082e97a6f3bba19dc6a343ba313eabcc21..c5570c8ee4119c1861d86e15aec1f7e24a4a35a9 100644
--- a/templates/base.qtpl.go
+++ b/templates/base.qtpl.go
@@ -71,174 +71,179 @@ func TimeFormat(t time.Time) string {
 	return t.Format("2006-01-02")
 }
 
+//line base.qtpl:33
+func Ignore[T any](v T, _ error) T {
+	return v
+}
+
 // Page prints a page implementing Page interface.
 
-//line base.qtpl:34
+//line base.qtpl:39
 func StreamPageTemplate(qw422016 *qt422016.Writer, p Page) {
-//line base.qtpl:34
+//line base.qtpl:39
 	qw422016.N().S(`
 <html lang="en">
     <head>
         <meta charset="utf-8">
         <link rel="icon" href="data:,">
         <title>cerrado | `)
-//line base.qtpl:39
+//line base.qtpl:44
 	p.StreamTitle(qw422016)
-//line base.qtpl:39
+//line base.qtpl:44
 	qw422016.N().S(`</title> 
         <link rel="stylesheet" href="/static/main`)
-//line base.qtpl:40
+//line base.qtpl:45
 	qw422016.E().S(Slug)
-//line base.qtpl:40
+//line base.qtpl:45
 	qw422016.N().S(`.css">
         <meta name="viewport" content="width=device-width, initial-scale=1" />
     </head>
     <body>
         `)
-//line base.qtpl:44
+//line base.qtpl:49
 	p.StreamNavbar(qw422016)
-//line base.qtpl:44
+//line base.qtpl:49
 	qw422016.N().S(`
         <div class="container">
             `)
-//line base.qtpl:46
+//line base.qtpl:51
 	p.StreamContent(qw422016)
-//line base.qtpl:46
+//line base.qtpl:51
 	qw422016.N().S(`
         </div>
     </body>
     `)
-//line base.qtpl:49
+//line base.qtpl:54
 	p.StreamScript(qw422016)
-//line base.qtpl:49
+//line base.qtpl:54
 	qw422016.N().S(`
 </html>
 `)
-//line base.qtpl:51
+//line base.qtpl:56
 }
 
-//line base.qtpl:51
+//line base.qtpl:56
 func WritePageTemplate(qq422016 qtio422016.Writer, p Page) {
-//line base.qtpl:51
+//line base.qtpl:56
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line base.qtpl:51
+//line base.qtpl:56
 	StreamPageTemplate(qw422016, p)
-//line base.qtpl:51
+//line base.qtpl:56
 	qt422016.ReleaseWriter(qw422016)
-//line base.qtpl:51
+//line base.qtpl:56
 }
 
-//line base.qtpl:51
+//line base.qtpl:56
 func PageTemplate(p Page) string {
-//line base.qtpl:51
+//line base.qtpl:56
 	qb422016 := qt422016.AcquireByteBuffer()
-//line base.qtpl:51
+//line base.qtpl:56
 	WritePageTemplate(qb422016, p)
-//line base.qtpl:51
+//line base.qtpl:56
 	qs422016 := string(qb422016.B)
-//line base.qtpl:51
+//line base.qtpl:56
 	qt422016.ReleaseByteBuffer(qb422016)
-//line base.qtpl:51
+//line base.qtpl:56
 	return qs422016
-//line base.qtpl:51
+//line base.qtpl:56
 }
 
-//line base.qtpl:53
+//line base.qtpl:58
 type BasePage struct{}
 
-//line base.qtpl:54
+//line base.qtpl:59
 func (p *BasePage) StreamTitle(qw422016 *qt422016.Writer) {
-//line base.qtpl:54
+//line base.qtpl:59
 	qw422016.N().S(`Empty`)
-//line base.qtpl:54
+//line base.qtpl:59
 }
 
-//line base.qtpl:54
+//line base.qtpl:59
 func (p *BasePage) WriteTitle(qq422016 qtio422016.Writer) {
-//line base.qtpl:54
+//line base.qtpl:59
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line base.qtpl:54
+//line base.qtpl:59
 	p.StreamTitle(qw422016)
-//line base.qtpl:54
+//line base.qtpl:59
 	qt422016.ReleaseWriter(qw422016)
-//line base.qtpl:54
+//line base.qtpl:59
 }
 
-//line base.qtpl:54
+//line base.qtpl:59
 func (p *BasePage) Title() string {
-//line base.qtpl:54
+//line base.qtpl:59
 	qb422016 := qt422016.AcquireByteBuffer()
-//line base.qtpl:54
+//line base.qtpl:59
 	p.WriteTitle(qb422016)
-//line base.qtpl:54
+//line base.qtpl:59
 	qs422016 := string(qb422016.B)
-//line base.qtpl:54
+//line base.qtpl:59
 	qt422016.ReleaseByteBuffer(qb422016)
-//line base.qtpl:54
+//line base.qtpl:59
 	return qs422016
-//line base.qtpl:54
+//line base.qtpl:59
 }
 
-//line base.qtpl:55
+//line base.qtpl:60
 func (p *BasePage) StreamBody(qw422016 *qt422016.Writer) {
-//line base.qtpl:55
+//line base.qtpl:60
 	qw422016.N().S(`HelloWorld`)
-//line base.qtpl:55
+//line base.qtpl:60
 }
 
-//line base.qtpl:55
+//line base.qtpl:60
 func (p *BasePage) WriteBody(qq422016 qtio422016.Writer) {
-//line base.qtpl:55
+//line base.qtpl:60
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line base.qtpl:55
+//line base.qtpl:60
 	p.StreamBody(qw422016)
-//line base.qtpl:55
+//line base.qtpl:60
 	qt422016.ReleaseWriter(qw422016)
-//line base.qtpl:55
+//line base.qtpl:60
 }
 
-//line base.qtpl:55
+//line base.qtpl:60
 func (p *BasePage) Body() string {
-//line base.qtpl:55
+//line base.qtpl:60
 	qb422016 := qt422016.AcquireByteBuffer()
-//line base.qtpl:55
+//line base.qtpl:60
 	p.WriteBody(qb422016)
-//line base.qtpl:55
+//line base.qtpl:60
 	qs422016 := string(qb422016.B)
-//line base.qtpl:55
+//line base.qtpl:60
 	qt422016.ReleaseByteBuffer(qb422016)
-//line base.qtpl:55
+//line base.qtpl:60
 	return qs422016
-//line base.qtpl:55
+//line base.qtpl:60
 }
 
-//line base.qtpl:56
+//line base.qtpl:61
 func (p *BasePage) StreamScript(qw422016 *qt422016.Writer) {
-//line base.qtpl:56
+//line base.qtpl:61
 }
 
-//line base.qtpl:56
+//line base.qtpl:61
 func (p *BasePage) WriteScript(qq422016 qtio422016.Writer) {
-//line base.qtpl:56
+//line base.qtpl:61
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line base.qtpl:56
+//line base.qtpl:61
 	p.StreamScript(qw422016)
-//line base.qtpl:56
+//line base.qtpl:61
 	qt422016.ReleaseWriter(qw422016)
-//line base.qtpl:56
+//line base.qtpl:61
 }
 
-//line base.qtpl:56
+//line base.qtpl:61
 func (p *BasePage) Script() string {
-//line base.qtpl:56
+//line base.qtpl:61
 	qb422016 := qt422016.AcquireByteBuffer()
-//line base.qtpl:56
+//line base.qtpl:61
 	p.WriteScript(qb422016)
-//line base.qtpl:56
+//line base.qtpl:61
 	qs422016 := string(qb422016.B)
-//line base.qtpl:56
+//line base.qtpl:61
 	qt422016.ReleaseByteBuffer(qb422016)
-//line base.qtpl:56
+//line base.qtpl:61
 	return qs422016
-//line base.qtpl:56
+//line base.qtpl:61
 }
diff --git a/templates/gititemblob.qtpl b/templates/gititemblob.qtpl
new file mode 100644
index 0000000000000000000000000000000000000000..89d002d2521e4b56c31db6811d137186f830614e
--- /dev/null
+++ b/templates/gititemblob.qtpl
@@ -0,0 +1,14 @@
+{% code
+type GitItemBlobPage struct {
+    File string
+    Content []byte
+}
+%}
+
+{% func (g *GitItemBlobPage) Nav(name, ref string) %}{%= GitItemNav(name, ref, Tree) %}{% endfunc %}
+
+{% func (g *GitItemBlobPage) GitContent() %}
+<div class="code-view">
+{%z= g.Content %}
+</div>
+{% endfunc %}
diff --git a/templates/gititemblob.qtpl.go b/templates/gititemblob.qtpl.go
new file mode 100644
index 0000000000000000000000000000000000000000..6b4e1878145e79855875577f6edc748cd6b5ea1c
--- /dev/null
+++ b/templates/gititemblob.qtpl.go
@@ -0,0 +1,98 @@
+// Code generated by qtc from "gititemblob.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+//line gititemblob.qtpl:1
+package templates
+
+//line gititemblob.qtpl:1
+import (
+	qtio422016 "io"
+
+	qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line gititemblob.qtpl:1
+var (
+	_ = qtio422016.Copy
+	_ = qt422016.AcquireByteBuffer
+)
+
+//line gititemblob.qtpl:2
+type GitItemBlobPage struct {
+	File    string
+	Content []byte
+}
+
+//line gititemblob.qtpl:8
+func (g *GitItemBlobPage) StreamNav(qw422016 *qt422016.Writer, name, ref string) {
+//line gititemblob.qtpl:8
+	StreamGitItemNav(qw422016, name, ref, Tree)
+//line gititemblob.qtpl:8
+}
+
+//line gititemblob.qtpl:8
+func (g *GitItemBlobPage) WriteNav(qq422016 qtio422016.Writer, name, ref string) {
+//line gititemblob.qtpl:8
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line gititemblob.qtpl:8
+	g.StreamNav(qw422016, name, ref)
+//line gititemblob.qtpl:8
+	qt422016.ReleaseWriter(qw422016)
+//line gititemblob.qtpl:8
+}
+
+//line gititemblob.qtpl:8
+func (g *GitItemBlobPage) Nav(name, ref string) string {
+//line gititemblob.qtpl:8
+	qb422016 := qt422016.AcquireByteBuffer()
+//line gititemblob.qtpl:8
+	g.WriteNav(qb422016, name, ref)
+//line gititemblob.qtpl:8
+	qs422016 := string(qb422016.B)
+//line gititemblob.qtpl:8
+	qt422016.ReleaseByteBuffer(qb422016)
+//line gititemblob.qtpl:8
+	return qs422016
+//line gititemblob.qtpl:8
+}
+
+//line gititemblob.qtpl:10
+func (g *GitItemBlobPage) StreamGitContent(qw422016 *qt422016.Writer) {
+//line gititemblob.qtpl:10
+	qw422016.N().S(`
+<div class="code-view">
+`)
+//line gititemblob.qtpl:12
+	qw422016.N().Z(g.Content)
+//line gititemblob.qtpl:12
+	qw422016.N().S(`
+</div>
+`)
+//line gititemblob.qtpl:14
+}
+
+//line gititemblob.qtpl:14
+func (g *GitItemBlobPage) WriteGitContent(qq422016 qtio422016.Writer) {
+//line gititemblob.qtpl:14
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line gititemblob.qtpl:14
+	g.StreamGitContent(qw422016)
+//line gititemblob.qtpl:14
+	qt422016.ReleaseWriter(qw422016)
+//line gititemblob.qtpl:14
+}
+
+//line gititemblob.qtpl:14
+func (g *GitItemBlobPage) GitContent() string {
+//line gititemblob.qtpl:14
+	qb422016 := qt422016.AcquireByteBuffer()
+//line gititemblob.qtpl:14
+	g.WriteGitContent(qb422016)
+//line gititemblob.qtpl:14
+	qs422016 := string(qb422016.B)
+//line gititemblob.qtpl:14
+	qt422016.ReleaseByteBuffer(qb422016)
+//line gititemblob.qtpl:14
+	return qs422016
+//line gititemblob.qtpl:14
+}
diff --git a/templates/gititemtree.qtpl b/templates/gititemtree.qtpl
index 5ace7b0ea7262684dcbb60ef67ce6f4249ff15e3..be7a27de050be204b7d90bac4fcffe073c44a921 100644
--- a/templates/gititemtree.qtpl
+++ b/templates/gititemtree.qtpl
@@ -1,10 +1,28 @@
+{% import "github.com/go-git/go-git/v5/plumbing/object" %}
+
 {% code
 type GitItemTreePage struct {
+    CurrentPath string
+    Tree *object.Tree
+
+    // TODO: remove this since it can be passed by GitCommit
+    Ref string
+    Name string
 }
 %}
 
 {% func (g *GitItemTreePage) Nav(name, ref string) %}{%= GitItemNav(name, ref, Tree) %}{% endfunc %}
 
 {% func (g *GitItemTreePage) GitContent() %}
-<h4>Tree</h4>
+{% for _, e := range g.Tree.Entries %}
+<div class="row">
+  <div class="col">{%s Ignore(e.Mode.ToOSFileMode()).String() %}</div>
+  {% if e.Mode.IsFile() %}
+  <div class="col-md"><a href="/{%s g.Name %}/blob/{%s g.Ref%}/{%s g.CurrentPath %}/{%s e.Name %}">{%s e.Name %}</a></div>
+  {% else %}
+  <div class="col-md"><a href="./{%s g.CurrentPath %}/{%s e.Name %}">{%s e.Name %}</a></div>
+  {% endif %}
+  <div class="col-md">{%dl Ignore(g.Tree.Size(e.Name))%} KiB</div>
+</div>
+{% endfor %}
 {% endfunc %}
diff --git a/templates/gititemtree.qtpl.go b/templates/gititemtree.qtpl.go
index d8beb0e09acb90fabbe5c2bb75de765952ca11a3..cdc374fef67dd51aedbd48aad507ea3ff515e48f 100644
--- a/templates/gititemtree.qtpl.go
+++ b/templates/gititemtree.qtpl.go
@@ -5,86 +5,165 @@ //line gititemtree.qtpl:1
 package templates
 
 //line gititemtree.qtpl:1
+import "github.com/go-git/go-git/v5/plumbing/object"
+
+//line gititemtree.qtpl:3
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line gititemtree.qtpl:1
+//line gititemtree.qtpl:3
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line gititemtree.qtpl:2
+//line gititemtree.qtpl:4
 type GitItemTreePage struct {
+	CurrentPath string
+	Tree        *object.Tree
+
+	// TODO: remove this since it can be passed by GitCommit
+	Ref  string
+	Name string
 }
 
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 func (g *GitItemTreePage) StreamNav(qw422016 *qt422016.Writer, name, ref string) {
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	StreamGitItemNav(qw422016, name, ref, Tree)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 }
 
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 func (g *GitItemTreePage) WriteNav(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	g.StreamNav(qw422016, name, ref)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	qt422016.ReleaseWriter(qw422016)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 }
 
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 func (g *GitItemTreePage) Nav(name, ref string) string {
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	qb422016 := qt422016.AcquireByteBuffer()
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	g.WriteNav(qb422016, name, ref)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	qs422016 := string(qb422016.B)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	qt422016.ReleaseByteBuffer(qb422016)
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 	return qs422016
-//line gititemtree.qtpl:6
+//line gititemtree.qtpl:14
 }
 
-//line gititemtree.qtpl:8
+//line gititemtree.qtpl:16
 func (g *GitItemTreePage) StreamGitContent(qw422016 *qt422016.Writer) {
-//line gititemtree.qtpl:8
+//line gititemtree.qtpl:16
+	qw422016.N().S(`
+`)
+//line gititemtree.qtpl:17
+	for _, e := range g.Tree.Entries {
+//line gititemtree.qtpl:17
+		qw422016.N().S(`
+<div class="row">
+  <div class="col">`)
+//line gititemtree.qtpl:19
+		qw422016.E().S(Ignore(e.Mode.ToOSFileMode()).String())
+//line gititemtree.qtpl:19
+		qw422016.N().S(`</div>
+  `)
+//line gititemtree.qtpl:20
+		if e.Mode.IsFile() {
+//line gititemtree.qtpl:20
+			qw422016.N().S(`
+  <div class="col-md"><a href="/`)
+//line gititemtree.qtpl:21
+			qw422016.E().S(g.Name)
+//line gititemtree.qtpl:21
+			qw422016.N().S(`/blob/`)
+//line gititemtree.qtpl:21
+			qw422016.E().S(g.Ref)
+//line gititemtree.qtpl:21
+			qw422016.N().S(`/`)
+//line gititemtree.qtpl:21
+			qw422016.E().S(g.CurrentPath)
+//line gititemtree.qtpl:21
+			qw422016.N().S(`/`)
+//line gititemtree.qtpl:21
+			qw422016.E().S(e.Name)
+//line gititemtree.qtpl:21
+			qw422016.N().S(`">`)
+//line gititemtree.qtpl:21
+			qw422016.E().S(e.Name)
+//line gititemtree.qtpl:21
+			qw422016.N().S(`</a></div>
+  `)
+//line gititemtree.qtpl:22
+		} else {
+//line gititemtree.qtpl:22
+			qw422016.N().S(`
+  <div class="col-md"><a href="./`)
+//line gititemtree.qtpl:23
+			qw422016.E().S(g.CurrentPath)
+//line gititemtree.qtpl:23
+			qw422016.N().S(`/`)
+//line gititemtree.qtpl:23
+			qw422016.E().S(e.Name)
+//line gititemtree.qtpl:23
+			qw422016.N().S(`">`)
+//line gititemtree.qtpl:23
+			qw422016.E().S(e.Name)
+//line gititemtree.qtpl:23
+			qw422016.N().S(`</a></div>
+  `)
+//line gititemtree.qtpl:24
+		}
+//line gititemtree.qtpl:24
+		qw422016.N().S(`
+  <div class="col-md">`)
+//line gititemtree.qtpl:25
+		qw422016.N().DL(Ignore(g.Tree.Size(e.Name)))
+//line gititemtree.qtpl:25
+		qw422016.N().S(` KiB</div>
+</div>
+`)
+//line gititemtree.qtpl:27
+	}
+//line gititemtree.qtpl:27
 	qw422016.N().S(`
-<h4>Tree</h4>
 `)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 }
 
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 func (g *GitItemTreePage) WriteGitContent(qq422016 qtio422016.Writer) {
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	g.StreamGitContent(qw422016)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	qt422016.ReleaseWriter(qw422016)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 }
 
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 func (g *GitItemTreePage) GitContent() string {
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	qb422016 := qt422016.AcquireByteBuffer()
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	g.WriteGitContent(qb422016)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	qs422016 := string(qb422016.B)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	qt422016.ReleaseByteBuffer(qb422016)
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 	return qs422016
-//line gititemtree.qtpl:10
+//line gititemtree.qtpl:28
 }
diff --git a/templates/navbar.qtpl b/templates/navbar.qtpl
index 775f496a4b52da8eef28656dedee80fe9995871d..9681fa4a5fd873e89621c1cdad5151f49661716b 100644
--- a/templates/navbar.qtpl
+++ b/templates/navbar.qtpl
@@ -25,7 +25,10 @@ {% func Navbar (s Selection) %}
         <nav class="container navbar navbar-expand">
           <div class="navbar-nav">
             <a class="nav-link{%= insertIfEqual(s, Git) %}" href="/">git</a>
+{% comment %}
+Add this back once needed
             <a class="nav-link{%= insertIfEqual(s, List) %}" href="/list">list</a>
+{% endcomment %}
             <a class="nav-link{%= insertIfEqual(s, About) %}" href="/about">about</a>
             <a class="nav-link{%= insertIfEqual(s, Config) %}" href="/config">config</a>
           </div>
diff --git a/templates/navbar.qtpl.go b/templates/navbar.qtpl.go
index cddc6a6de17621590eb849a2ef2a27b84c661715..a2989db1381d0e7ff34c0499825bc27a2b303271 100644
--- a/templates/navbar.qtpl.go
+++ b/templates/navbar.qtpl.go
@@ -86,167 +86,165 @@ //line navbar.qtpl:27
 	streaminsertIfEqual(qw422016, s, Git)
 //line navbar.qtpl:27
 	qw422016.N().S(`" href="/">git</a>
+`)
+//line navbar.qtpl:31
+	qw422016.N().S(`
             <a class="nav-link`)
-//line navbar.qtpl:28
-	streaminsertIfEqual(qw422016, s, List)
-//line navbar.qtpl:28
-	qw422016.N().S(`" href="/list">list</a>
-            <a class="nav-link`)
-//line navbar.qtpl:29
+//line navbar.qtpl:32
 	streaminsertIfEqual(qw422016, s, About)
-//line navbar.qtpl:29
+//line navbar.qtpl:32
 	qw422016.N().S(`" href="/about">about</a>
             <a class="nav-link`)
-//line navbar.qtpl:30
+//line navbar.qtpl:33
 	streaminsertIfEqual(qw422016, s, Config)
-//line navbar.qtpl:30
+//line navbar.qtpl:33
 	qw422016.N().S(`" href="/config">config</a>
           </div>
         </nav>
 `)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 }
 
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 func WriteNavbar(qq422016 qtio422016.Writer, s Selection) {
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	StreamNavbar(qw422016, s)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	qt422016.ReleaseWriter(qw422016)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 }
 
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 func Navbar(s Selection) string {
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	qb422016 := qt422016.AcquireByteBuffer()
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	WriteNavbar(qb422016, s)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	qs422016 := string(qb422016.B)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	qt422016.ReleaseByteBuffer(qb422016)
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 	return qs422016
-//line navbar.qtpl:33
+//line navbar.qtpl:36
 }
 
-//line navbar.qtpl:35
+//line navbar.qtpl:38
 func StreamGitItemNav(qw422016 *qt422016.Writer, name, ref string, s GitSelection) {
-//line navbar.qtpl:35
+//line navbar.qtpl:38
 	qw422016.N().S(`
 <div class="row">
     <h3>`)
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 	qw422016.E().S(name)
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 	qw422016.N().S(` `)
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 	if ref != "" && (s == Log || s == Tree) {
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 		qw422016.N().S(`@ `)
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 		qw422016.E().S(ref)
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 	}
-//line navbar.qtpl:37
+//line navbar.qtpl:40
 	qw422016.N().S(`</h3>
 </div>
 <div class="row">
   <ul class="nav">
     <li class="nav-item">
       <a class="nav-link`)
-//line navbar.qtpl:42
-	streaminsertIfEqual(qw422016, s, Readme)
-//line navbar.qtpl:42
-	qw422016.N().S(`" aria-current="page" href="/`)
-//line navbar.qtpl:42
-	qw422016.E().S(name)
-//line navbar.qtpl:42
-	qw422016.N().S(`/about">about</a>
-    </li>
-    <li class="nav-item">
-      <a class="nav-link`)
 //line navbar.qtpl:45
-	streaminsertIfEqual(qw422016, s, Log)
+	streaminsertIfEqual(qw422016, s, Readme)
 //line navbar.qtpl:45
 	qw422016.N().S(`" aria-current="page" href="/`)
 //line navbar.qtpl:45
 	qw422016.E().S(name)
 //line navbar.qtpl:45
-	qw422016.N().S(`/log/`)
-//line navbar.qtpl:45
-	qw422016.E().S(ref)
-//line navbar.qtpl:45
-	qw422016.N().S(`">log</a>
+	qw422016.N().S(`/about">about</a>
     </li>
     <li class="nav-item">
       <a class="nav-link`)
 //line navbar.qtpl:48
-	streaminsertIfEqual(qw422016, s, Summary)
+	streaminsertIfEqual(qw422016, s, Log)
 //line navbar.qtpl:48
 	qw422016.N().S(`" aria-current="page" href="/`)
 //line navbar.qtpl:48
 	qw422016.E().S(name)
 //line navbar.qtpl:48
-	qw422016.N().S(`">summary</a>
+	qw422016.N().S(`/log/`)
+//line navbar.qtpl:48
+	qw422016.E().S(ref)
+//line navbar.qtpl:48
+	qw422016.N().S(`">log</a>
     </li>
     <li class="nav-item">
       <a class="nav-link`)
 //line navbar.qtpl:51
-	streaminsertIfEqual(qw422016, s, Refs)
+	streaminsertIfEqual(qw422016, s, Summary)
 //line navbar.qtpl:51
 	qw422016.N().S(`" aria-current="page" href="/`)
 //line navbar.qtpl:51
 	qw422016.E().S(name)
 //line navbar.qtpl:51
-	qw422016.N().S(`/refs">refs</a>
+	qw422016.N().S(`">summary</a>
     </li>
     <li class="nav-item">
       <a class="nav-link`)
 //line navbar.qtpl:54
-	streaminsertIfEqual(qw422016, s, Tree)
+	streaminsertIfEqual(qw422016, s, Refs)
 //line navbar.qtpl:54
 	qw422016.N().S(`" aria-current="page" href="/`)
 //line navbar.qtpl:54
 	qw422016.E().S(name)
 //line navbar.qtpl:54
+	qw422016.N().S(`/refs">refs</a>
+    </li>
+    <li class="nav-item">
+      <a class="nav-link`)
+//line navbar.qtpl:57
+	streaminsertIfEqual(qw422016, s, Tree)
+//line navbar.qtpl:57
+	qw422016.N().S(`" aria-current="page" href="/`)
+//line navbar.qtpl:57
+	qw422016.E().S(name)
+//line navbar.qtpl:57
 	qw422016.N().S(`/tree/`)
-//line navbar.qtpl:54
+//line navbar.qtpl:57
 	qw422016.E().S(ref)
-//line navbar.qtpl:54
+//line navbar.qtpl:57
 	qw422016.N().S(`">tree</a>
     </li>
   </ul>
 </div>
 `)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 }
 
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 func WriteGitItemNav(qq422016 qtio422016.Writer, name, ref string, s GitSelection) {
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	StreamGitItemNav(qw422016, name, ref, s)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	qt422016.ReleaseWriter(qw422016)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 }
 
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 func GitItemNav(name, ref string, s GitSelection) string {
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	qb422016 := qt422016.AcquireByteBuffer()
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	WriteGitItemNav(qb422016, name, ref, s)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	qs422016 := string(qb422016.B)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	qt422016.ReleaseByteBuffer(qb422016)
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 	return qs422016
-//line navbar.qtpl:58
+//line navbar.qtpl:61
 }