Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-07-18 12:40:45
Commit8abd3436c0a348d9a084e4a83a16e028ca2e8199
Parente08e053ce424bcb899dfb39b0028011762819ce9

Implement repository refs page

Diffstat

M main.go | 14 ++++++++++----
M res/admin_user_index.html | 1 +
A res/error.html | 11 +++++++++++
M res/repo_create.html | 3 ++-
M res/repo_index.html | 1 +
M res/repo_log.html | 21 ++++++++++++++++++++-
A res/repo_refs.html | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A res/repo_tree.html | 44 ++++++++++++++++++++++++++++++++++++++++++++
M res/res.go | 9 +++++++++
M res/style.css | 6 ++++--
M res/user_create.html | 1 +
M res/user_login.html | 3 ++-
M src/admin.go | 22 +++++++++++-----------
M src/git.go | 18 +++++++++---------
A src/http.go | 21 +++++++++++++++++++++
M src/repo.go | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
M src/user.go | 15 +++++----------

17 files changed, 310 insertions, 61 deletions

diff --git a/main.go b/main.go
index 2977ff6..0ef7922 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,7 @@ package main
 import (
 	"log"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/Jamozed/Goit/res"
@@ -37,17 +38,18 @@ func main() {
 	h.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(g.HandleAdminUserCreate)
 	// h.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc()
 
-	h.Path("/{repo}/").Methods(http.MethodGet).HandlerFunc(g.HandleRepoLog)
+	h.Path("/{repo:.+(?:\\.git)$}").Methods(http.MethodGet).HandlerFunc(redirectDotGit)
+	h.Path("/{repo}").Methods(http.MethodGet).HandlerFunc(g.HandleRepoLog)
 	h.Path("/{repo}/log").Methods(http.MethodGet).HandlerFunc(g.HandleRepoLog)
-	// h.Path("/{repo}/tree").Methods(http.MethodGet).HandlerFunc(g.HandleRepoTree)
-	// h.Path("/{repo}/refs").Methods(http.MethodGet).HandlerFunc(g.HandleRepoRefs)
+	h.Path("/{repo}/tree").Methods(http.MethodGet).HandlerFunc(g.HandleRepoTree)
+	h.Path("/{repo}/refs").Methods(http.MethodGet).HandlerFunc(g.HandleRepoRefs)
 	h.Path("/{repo}/info/refs").Methods(http.MethodGet).HandlerFunc(goit.HandleInfoRefs)
 	h.Path("/{repo}/git-upload-pack").Methods(http.MethodPost).HandlerFunc(goit.HandleUploadPack)
 	h.Path("/{repo}/git-receive-pack").Methods(http.MethodPost).HandlerFunc(goit.HandleReceivePack)
 
 	h.Path("/static/style.css").Methods(http.MethodGet).HandlerFunc(handleStyle)
 
-	h.PathPrefix("/").HandlerFunc(http.NotFound)
+	h.PathPrefix("/").HandlerFunc(goit.HttpNotFound)
 
 	/* Create a ticker to periodically cleanup expired sessions */
 	tick := time.NewTicker(1 * time.Hour)
@@ -75,3 +77,7 @@ func handleStyle(w http.ResponseWriter, r *http.Request) {
 		log.Println("[handleStyle]", err.Error())
 	}
 }
+
+func redirectDotGit(w http.ResponseWriter, r *http.Request) {
+	http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, ".git"), http.StatusMovedPermanently)
+}
diff --git a/res/admin_user_index.html b/res/admin_user_index.html
index ed84ecd..08bc819 100644
--- a/res/admin_user_index.html
+++ b/res/admin_user_index.html
@@ -4,6 +4,7 @@
 	<title>Users</title>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
 </head>
 <body>
 	<table>
diff --git a/res/error.html b/res/error.html
new file mode 100644
index 0000000..1a00cdc
--- /dev/null
+++ b/res/error.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<head>
+	<meta charset="UTF-8">
+	<title>{{.Status}}</title>
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
+</head>
+<body>
+	<b>{{.Status}}</b>
+</body>
diff --git a/res/repo_create.html b/res/repo_create.html
index dd04bad..7fd3740 100644
--- a/res/repo_create.html
+++ b/res/repo_create.html
@@ -3,7 +3,8 @@
 	<meta charset="UTF-8">
 	<title>Create Repository</title>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
-	<link rel="stylesheet" href="/static/style.css">
+	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
 </head>
 <body>
 	<form action="/repo/create" method="post">
diff --git a/res/repo_index.html b/res/repo_index.html
index 450097b..2662bd3 100644
--- a/res/repo_index.html
+++ b/res/repo_index.html
@@ -4,6 +4,7 @@
 	<title>Repositories</title>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
 </head>
 <body>
 	<table>
diff --git a/res/repo_log.html b/res/repo_log.html
index 4dbd5b1..113d825 100644
--- a/res/repo_log.html
+++ b/res/repo_log.html
@@ -1,11 +1,30 @@
 <!DOCTYPE html>
 <head>
 	<meta charset="UTF-8">
-	<title>Repositories</title>
+	<title>Log</title>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
 </head>
 <body>
+	<table>
+		<tr><td><h1>{{.Name}}</h1></td></tr>
+		<tr><td><span>{{.Description}}</span></td></tr>
+		<tr><td>git clone <a href="{{.Url}}">{{.Url}}</a></td></tr>
+		<tr>
+			<td>
+				<a href="/{{.Name}}/log">Log</a>
+				| <a href="/{{.Name}}/tree">Tree</a>
+				| <a href="/{{.Name}}/refs">Refs</a>
+				{{if .HasReadme}}
+					| <a href="">README</a>
+				{{end}}
+				{{if .HasLicence}}
+					| <a href="">LICENCE</a>
+				{{end}}
+			</td>
+		</tr>
+	</table><hr>
 	<table>
 		<thead>
 			<tr>
diff --git a/res/repo_refs.html b/res/repo_refs.html
new file mode 100644
index 0000000..19fc5d3
--- /dev/null
+++ b/res/repo_refs.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<head>
+	<meta charset="UTF-8">
+	<title>Refs</title>
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
+</head>
+<body>
+	<table>
+		<tr><td><h1>{{.Name}}</h1></td></tr>
+		<tr><td><span>{{.Description}}</span></td></tr>
+		<tr><td>git clone <a href="{{.Url}}">{{.Url}}</a></td></tr>
+		<tr>
+			<td>
+				<a href="/{{.Name}}/log">Log</a>
+				| <a href="/{{.Name}}/tree">Tree</a>
+				| <a href="/{{.Name}}/refs">Refs</a>
+				{{if .HasReadme}}
+					| <a href="">README</a>
+				{{end}}
+				{{if .HasLicence}}
+					| <a href="">LICENCE</a>
+				{{end}}
+			</td>
+		</tr>
+	</table><hr>
+	<h2>Branches</h2>
+	<table>
+		<thead>
+			<tr>
+				<td><b>Name</b></td>
+				<td><b>Hash</b></td>
+			</tr>
+		</thead>
+		<tbody>
+		{{range .Branches}}
+			<tr>
+				<td>{{.Name}}</a></td>
+				<td>{{.Hash}}</td>
+			</tr>
+		{{end}}
+		</tbody>
+	</table>
+	<h2>Tags</h2>
+	<table>
+		<thead>
+			<tr>
+				<td><b>Name</b></td>
+				<td><b>Hash</b></td>
+			</tr>
+		</thead>
+		<tbody>
+		{{range .Tags}}
+			<tr>
+				<td>{{.Name}}</a></td>
+				<td>{{.Hash}}</td>
+			</tr>
+		{{end}}
+		</tbody>
+	</table>
+</body>
diff --git a/res/repo_tree.html b/res/repo_tree.html
new file mode 100644
index 0000000..ca65c95
--- /dev/null
+++ b/res/repo_tree.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<head>
+	<meta charset="UTF-8">
+	<title>Tree</title>
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
+</head>
+<body>
+	<table>
+		<tr><td><h1>{{.Name}}</h1></td></tr>
+		<tr><td><span>{{.Description}}</span></td></tr>
+		<tr><td>git clone <a href="{{.Url}}">{{.Url}}</a></td></tr>
+		<tr>
+			<td>
+				<a href="/{{.Name}}/log">Log</a>
+				| <a href="/{{.Name}}/tree">Tree</a>
+				| <a href="/{{.Name}}/refs">Refs</a>
+				{{if .HasReadme}}
+					| <a href="">README</a>
+				{{end}}
+				{{if .HasLicence}}
+					| <a href="">LICENCE</a>
+				{{end}}
+			</td>
+		</tr>
+	</table><hr>
+	<table>
+		<thead>
+			<tr>
+				<td><b>Name</b></td>
+				<td><b>Size</b></td>
+			</tr>
+		</thead>
+		<tbody>
+		{{range .Files}}
+			<tr>
+				<td>{{.Name}}</a></td>
+				<td>{{.Size}}</td>
+			</tr>
+		{{end}}
+		</tbody>
+	</table>
+</body>
diff --git a/res/res.go b/res/res.go
index 3acb4e9..cb3647b 100644
--- a/res/res.go
+++ b/res/res.go
@@ -14,11 +14,20 @@ var RepoCreate string
 //go:embed repo_log.html
 var RepoLog string
 
+//go:embed repo_tree.html
+var RepoTree string
+
+//go:embed repo_refs.html
+var RepoRefs string
+
 //go:embed user_create.html
 var UserCreate string
 
 //go:embed admin_user_index.html
 var AdminUserIndex string
 
+//go:embed error.html
+var Error string
+
 //go:embed style.css
 var Style string
diff --git a/res/style.css b/res/style.css
index 2f78065..28108ed 100644
--- a/res/style.css
+++ b/res/style.css
@@ -1,8 +1,10 @@
 html { background-color: #111111; color: #888888; height: 100%; }
-body { font-family: monospace; margin: 0; width: 100%; }
+body { font-family: monospace; margin: 1em; }
+
 a { color: #FF7E00; text-decoration: none; }
 a:hover { text-decoration: underline; }
+h1 { font-size: 1em; margin: 0; }
+hr { border: 0; height: 1em; margin: 0; }
 
-table { margin: 1em; }
 table td { padding: 0 0.4em; }
 table tr:hover td { background-color: #222222; }
diff --git a/res/user_create.html b/res/user_create.html
index b04c0c3..c4cfbd7 100644
--- a/res/user_create.html
+++ b/res/user_create.html
@@ -4,6 +4,7 @@
 	<title>Create User</title>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
 </head>
 <body>
 	<h1>Create User</h1>
diff --git a/res/user_login.html b/res/user_login.html
index 1ade9a1..0da5c64 100644
--- a/res/user_login.html
+++ b/res/user_login.html
@@ -3,7 +3,8 @@
 	<meta charset="UTF-8">
 	<title>Login</title>
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
-	<link rel="stylesheet" href="/static/style.css">
+	<link rel="stylesheet" type="text/css" href="/static/style.css">
+	<link rel="icon" type="image/png" href="/static/favicon.png">
 </head>
 <body>
 	<h1>Login</h1>
diff --git a/src/admin.go b/src/admin.go
index 6f5a75e..cb8f762 100644
--- a/src/admin.go
+++ b/src/admin.go
@@ -24,20 +24,20 @@ func init() {
 
 func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) {
 	if ok, uid := AuthHttp(r); !ok {
-		http.NotFound(w, r)
+		HttpError(w, http.StatusNotFound)
 		return
 	} else if user, err := g.GetUser(uid); err != nil {
 		log.Println("[Admin:User:Create:Auth]", err.Error())
-		http.NotFound(w, r)
+		HttpError(w, http.StatusNotFound)
 		return
 	} else if !user.IsAdmin {
-		http.NotFound(w, r)
+		HttpError(w, http.StatusNotFound)
 		return
 	}
 
 	if rows, err := g.db.Query("SELECT id, name, name_full, is_admin FROM users"); err != nil {
 		log.Println("[Admin:User:Index:SELECT]", err.Error())
-		http.Error(w, "500 internal server error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
 	} else {
 		defer rows.Close()
 
@@ -56,7 +56,7 @@ func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) {
 
 		if err := rows.Err(); err != nil {
 			log.Println("[Admin:User:Index:SELECT:Err]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 		} else {
 			adminUserIndex.Execute(w, struct{ Users []row }{users})
 		}
@@ -65,14 +65,14 @@ func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) {
 
 func (g *Goit) HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
 	if ok, uid := AuthHttp(r); !ok {
-		http.NotFound(w, r)
+		HttpError(w, http.StatusNotFound)
 		return
 	} else if user, err := g.GetUser(uid); err != nil {
 		log.Println("[Admin:User:Create:Auth]", err.Error())
-		http.NotFound(w, r)
+		HttpError(w, http.StatusNotFound)
 		return
 	} else if !user.IsAdmin {
-		http.NotFound(w, r)
+		HttpError(w, http.StatusNotFound)
 		return
 	}
 
@@ -90,20 +90,20 @@ func (g *Goit) HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
 			data.Msg = "Username \"" + username + "\" is reserved"
 		} else if exists, err := g.UserExists(username); err != nil {
 			log.Println("[Admin:User:Create:Exists]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 			return
 		} else if exists {
 			data.Msg = "Username \"" + username + "\" is taken"
 		} else if salt, err := Salt(); err != nil {
 			log.Println("[Admin:User:Create:Salt]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 			return
 		} else if _, err := g.db.Exec(
 			"INSERT INTO users (name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?)",
 			username, fullname, Hash(password, salt), "argon2", salt, admin,
 		); err != nil {
 			log.Println("[Admin:User:Create:INSERT]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 			return
 		} else {
 			data.Msg = "User \"" + username + "\" created successfully"
diff --git a/src/git.go b/src/git.go
index a5c6373..cd7dcbf 100644
--- a/src/git.go
+++ b/src/git.go
@@ -40,7 +40,7 @@ func HandleInfoRefs(w http.ResponseWriter, r *http.Request) {
 	refs, _, err := c.Run(nil, nil)
 	if err != nil {
 		log.Println("[Git]", err.Error())
-		http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
 		return
 	}
 
@@ -83,28 +83,28 @@ func httpBase(w http.ResponseWriter, r *http.Request, service string) *Repo {
 	case "git-receive-pack":
 		isPull = false
 	default:
-		http.Error(w, "404 Not Found", http.StatusNotFound)
+		HttpError(w, http.StatusNotFound)
 		return nil
 	}
 
 	if r.Header.Get("Git-Protocol") != "version=2" {
-		http.Error(w, "403 Forbidden", http.StatusForbidden)
+		HttpError(w, http.StatusForbidden)
 		return nil
 	}
 
 	repo, err := GetRepoByName(db, reponame)
 	if err != nil {
-		http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
 		return nil
 	} else if repo == nil {
-		http.Error(w, "404 Not Found", http.StatusNotFound)
+		HttpError(w, http.StatusNotFound)
 		return nil
 	}
 
 	/* Require authentication other than for public pull */
 	if repo.IsPrivate || !isPull {
 		/* TODO authentcate */
-		http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
+		HttpError(w, http.StatusUnauthorized)
 		return nil
 	}
 
@@ -120,7 +120,7 @@ func serviceRPC(w http.ResponseWriter, r *http.Request, service string, repo *Re
 
 	if r.Header.Get("Content-Type") != "application/x-"+service+"-request" {
 		log.Println("[GitRPC]", "Content-Type mismatch")
-		http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
+		HttpError(w, http.StatusUnauthorized)
 		return
 	}
 
@@ -128,7 +128,7 @@ func serviceRPC(w http.ResponseWriter, r *http.Request, service string, repo *Re
 	if r.Header.Get("Content-Encoding") == "gzip" {
 		if b, err := gzip.NewReader(r.Body); err != nil {
 			log.Println("[GitRPC]", err.Error())
-			http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 			return
 		} else {
 			body = b
@@ -145,7 +145,7 @@ func serviceRPC(w http.ResponseWriter, r *http.Request, service string, repo *Re
 
 	if _, _, err := c.Run(body, w); err != nil {
 		log.Println("[GitRPC]", err.Error())
-		http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
 		return
 	}
 }
diff --git a/src/http.go b/src/http.go
new file mode 100644
index 0000000..d2e2162
--- /dev/null
+++ b/src/http.go
@@ -0,0 +1,21 @@
+package goit
+
+import (
+	"fmt"
+	"html/template"
+	"net/http"
+
+	"github.com/Jamozed/Goit/res"
+)
+
+var htmlError *template.Template = template.Must(template.New("error").Parse(res.Error))
+
+func HttpError(w http.ResponseWriter, code int) {
+	w.WriteHeader(code)
+	s := fmt.Sprint(code) + " " + http.StatusText(code)
+	htmlError.Execute(w, struct{ Status string }{s})
+}
+
+func HttpNotFound(w http.ResponseWriter, r *http.Request) {
+	HttpError(w, http.StatusNotFound)
+}
diff --git a/src/repo.go b/src/repo.go
index bfbf8ce..01a6fca 100644
--- a/src/repo.go
+++ b/src/repo.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/Jamozed/Goit/res"
 	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/gorilla/mux"
 )
@@ -30,23 +31,19 @@ type Repo struct {
 }
 
 var (
-	repoIndex  *template.Template
-	repoCreate *template.Template
-	repoLog    *template.Template
+	repoIndex  *template.Template = template.Must(template.New("repo_index").Parse(res.RepoIndex))
+	repoCreate *template.Template = template.Must(template.New("repo_create").Parse(res.RepoCreate))
+	repoLog    *template.Template = template.Must(template.New("repo_log").Parse(res.RepoLog))
+	repoTree   *template.Template = template.Must(template.New("repo_tree").Parse(res.RepoTree))
+	repoRefs   *template.Template = template.Must(template.New("repo_refs").Parse(res.RepoRefs))
 )
 
-func init() {
-	repoIndex = template.Must(template.New("repo_index").Parse(res.RepoIndex))
-	repoCreate = template.Must(template.New("repo_create").Parse(res.RepoCreate))
-	repoLog = template.Must(template.New("repo_log").Parse(res.RepoLog))
-}
-
 func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) {
 	authOk, uid := AuthHttp(r)
 
 	if rows, err := g.db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil {
 		log.Println("[Index:SELECT]", err.Error())
-		http.Error(w, "500 internal server error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
 	} else {
 		defer rows.Close()
 
@@ -70,7 +67,7 @@ func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) {
 
 		if err := rows.Err(); err != nil {
 			log.Println("[Index:SELECT:Err]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 		} else {
 			repoIndex.Execute(w, struct{ Repos []row }{repos})
 		}
@@ -79,14 +76,14 @@ func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) {
 
 func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
 	if ok, uid := AuthHttp(r); !ok {
-		http.Error(w, "401 unauthorized", http.StatusUnauthorized)
+		HttpError(w, http.StatusUnauthorized)
 	} else if r.Method == http.MethodPost {
 		name := r.FormValue("reponame")
 		private := r.FormValue("visibility") == "private"
 
 		if taken, err := RepoExists(g.db, name); err != nil {
 			log.Println("[RepoCreate:RepoExists]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 		} else if taken {
 			repoCreate.Execute(w, struct{ Msg string }{"Reponame is taken"})
 		} else if SliceContains[string](reserved, name) {
@@ -99,7 +96,7 @@ func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
 				uid, name, strings.ToLower(name), "", "master", private,
 			); err != nil {
 				log.Println("[RepoCreate:INSERT]", err.Error())
-				http.Error(w, "500 internal server error", http.StatusInternalServerError)
+				HttpError(w, http.StatusInternalServerError)
 			} else {
 				http.Redirect(w, r, "/"+name+"/", http.StatusFound)
 			}
@@ -112,27 +109,105 @@ func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
 func (g *Goit) HandleRepoLog(w http.ResponseWriter, r *http.Request) {
 	reponame := mux.Vars(r)["repo"]
 
+	repo, err := GetRepoByName(db, reponame)
+	if err != nil {
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if repo == nil {
+		HttpError(w, http.StatusNotFound)
+		return
+	}
+
 	type row struct{ Date, Message, Author string }
 	commits := []row{}
 
 	if gr, err := git.PlainOpen("./" + reponame + ".git"); err != nil {
-		log.Println("[Repo:Open]", err.Error())
-		http.Error(w, "500 internal server error", http.StatusInternalServerError)
+		log.Println("[Repo:Log]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
 	} else if ref, err := gr.Head(); err != nil {
-		log.Println("[Repo:Head]", err.Error())
-		http.Error(w, "500 internal server error", http.StatusInternalServerError)
+		log.Println("[Repo:Log]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
 	} else if iter, err := gr.Log(&git.LogOptions{From: ref.Hash()}); err != nil {
 		log.Println("[Repo:Log]", err.Error())
-		http.Error(w, "500 internal server error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
+		return
 	} else if err := iter.ForEach(func(c *object.Commit) error {
-		commits = append(commits, row{c.Author.When.UTC().Format(time.RFC3339), c.Message, c.Author.Name})
+		commits = append(commits, row{c.Author.When.UTC().Format(time.DateTime), strings.SplitN(c.Message, "\n", 2)[0], c.Author.Name})
 		return nil
 	}); err != nil {
 		log.Println("[Repo:Log]", err.Error())
-		http.Error(w, "500 internal server error", http.StatusInternalServerError)
+		HttpError(w, http.StatusInternalServerError)
+		return
 	}
 
-	repoLog.Execute(w, struct{ Commits []row }{commits})
+	if err := repoLog.Execute(w, struct {
+		Name, Description, Url string
+		HasReadme, HasLicence  bool
+		Commits                []row
+	}{reponame, repo.Description, r.URL.Host + "/" + repo.Name + ".git", false, false, commits}); err != nil {
+		log.Println("[Repo:Log]", err.Error())
+	}
+}
+
+func (g *Goit) HandleRepoTree(w http.ResponseWriter, r *http.Request) {
+	HttpError(w, http.StatusNoContent)
+}
+
+func (g *Goit) HandleRepoRefs(w http.ResponseWriter, r *http.Request) {
+	reponame := mux.Vars(r)["repo"]
+
+	repo, err := GetRepoByName(db, reponame)
+	if err != nil {
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if repo == nil {
+		HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	type bra struct{ Name, Hash string }
+	type tag struct{ Name, Hash string }
+	bras := []bra{}
+	tags := []tag{}
+
+	if gr, err := git.PlainOpen("./" + reponame + ".git"); err != nil {
+		log.Println("[Repo:Refs]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if iter, err := gr.Branches(); err != nil {
+		log.Println("[Repo:Refs]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if err := iter.ForEach(func(b *plumbing.Reference) error {
+		bras = append(bras, bra{b.Name().Short(), b.Hash().String()})
+		return nil
+	}); err != nil {
+		log.Println("[Repo:Refs]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if iter, err := gr.Tags(); err != nil {
+		log.Println("[Repo:Refs]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	} else if err := iter.ForEach(func(b *plumbing.Reference) error {
+		tags = append(tags, tag{b.Name().Short(), b.Hash().String()})
+		return nil
+	}); err != nil {
+		log.Println("[Repo:Refs]", err.Error())
+		HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	if err := repoRefs.Execute(w, struct {
+		Name, Description, Url string
+		HasReadme, HasLicence  bool
+		Branches               []bra
+		Tags                   []tag
+	}{reponame, repo.Description, r.URL.Host + "/" + repo.Name + ".git", false, false, bras, tags}); err != nil {
+		log.Println("[Repo:Refs]", err.Error())
+	}
 }
 
 func GetRepoByName(db *sql.DB, name string) (*Repo, error) {
diff --git a/src/user.go b/src/user.go
index 53e7653..9b2617c 100644
--- a/src/user.go
+++ b/src/user.go
@@ -31,15 +31,10 @@ type User struct {
 var (
 	reserved []string = []string{"admin", "repo", "static", "user"}
 
-	userLogin  *template.Template
-	userCreate *template.Template
+	userLogin  *template.Template = template.Must(template.New("user_login").Parse(res.UserLogin))
+	userCreate *template.Template = template.Must(template.New("user_create").Parse(res.UserCreate))
 )
 
-func init() {
-	userLogin = template.Must(template.New("user_login").Parse(res.UserLogin))
-	userCreate = template.Must(template.New("user_create").Parse(res.UserCreate))
-}
-
 func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) {
 	if ok, _ := AuthHttp(r); ok {
 		http.Redirect(w, r, "/", http.StatusFound)
@@ -57,7 +52,7 @@ func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) {
 			data.Msg = "Username cannot be empty"
 		} else if exists, err := g.UserExists(username); err != nil {
 			log.Println("[User:Login:Exists]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 			return
 		} else if !exists {
 			data.Msg = "Invalid credentials"
@@ -65,7 +60,7 @@ func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) {
 			"SELECT id, name, pass, pass_algo, salt FROM users WHERE name = ?", username,
 		).Scan(&u.Id, &u.Name, &u.Pass, &u.PassAlgo, &u.Salt); err != nil {
 			log.Println("[User:Login:SELECT]", err.Error())
-			http.Error(w, "500 internal server error", http.StatusInternalServerError)
+			HttpError(w, http.StatusInternalServerError)
 			return
 		} else if !bytes.Equal(Hash(password, u.Salt), u.Pass) {
 			data.Msg = "Invalid credentials"
@@ -73,7 +68,7 @@ func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) {
 			expiry := time.Now().Add(15 * time.Minute)
 			if s, err := NewSession(u.Id, expiry); err != nil {
 				log.Println("[User:Login:Session]", err.Error())
-				http.Error(w, "500 internal server error", http.StatusInternalServerError)
+				HttpError(w, http.StatusInternalServerError)
 				return
 			} else {
 				http.SetCookie(w, &http.Cookie{Name: "session", Value: s, Path: "/", Expires: expiry})