package handlers
import (
"fmt"
"html/template"
"net/http"
"net/url"
"repobrowser/internal/config"
"repobrowser/internal/git"
"repobrowser/internal/models"
"repobrowser/internal/utils"
"strings"
"github.com/gin-gonic/gin"
)
// GET / — list all repos
func HandleRepoList(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repos := git.ListRepos(cfg)
var body strings.Builder
body.WriteString(fmt.Sprintf(
`
Repositories
`+
`%s · %d repositories
`,
template.HTMLEscapeString(cfg.Root), len(repos),
))
if len(repos) == 0 {
body.WriteString(`No git repositories found.
`)
} else {
body.WriteString(``)
for _, r := range repos {
meta := r.Branch
if r.LastCommit != "" {
meta += " · " + template.HTMLEscapeString(r.LastCommit)
}
body.WriteString(fmt.Sprintf(
`
`+
`📦 %s
`+
`%s
`+
`%s
`+
``,
url.PathEscape(r.Name),
template.HTMLEscapeString(r.Name),
template.HTMLEscapeString(r.Description),
meta,
))
}
body.WriteString("
")
}
utils.RenderPage(c, models.PageData{
Title: "gitweb",
Breadcrumb: template.HTML(`repositories`),
GitCmd: fmt.Sprintf("ls %s", cfg.Root),
Body: template.HTML(body.String()),
})
}
}
// GET /:repo/ — file list
func HandleIndex(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repoName, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
pd := utils.BasePageData(c, repoName, rp, "files",
template.HTML(fmt.Sprintf(
`repos / %s`,
url.PathEscape(repoName), template.HTMLEscapeString(repoName),
)))
files, cmd, err := git.ListFiles(rp, pd.Branch)
var body strings.Builder
body.WriteString(fmt.Sprintf(
`%s
`+
`%s · branch: %s · %d files
`,
template.HTMLEscapeString(repoName), template.HTMLEscapeString(rp),
template.HTMLEscapeString(pd.Branch), len(files),
))
if err != nil || len(files) == 0 {
body.WriteString(`No tracked files found on this branch.
`)
} else {
body.WriteString(`| File | Commits |
`)
for _, f := range files {
parts := strings.Split(f.Path, "/")
name := parts[len(parts)-1]
dir := ""
if len(parts) > 1 {
dir = strings.Join(parts[:len(parts)-1], "/") + "/"
}
body.WriteString(fmt.Sprintf(
`| %s`+
`%s`+
`%s | `+
`%d |
`,
git.FileIcon(name), template.HTMLEscapeString(dir),
url.PathEscape(repoName), url.QueryEscape(f.Path), url.QueryEscape(pd.Branch),
template.HTMLEscapeString(name), f.CommitCount,
))
}
body.WriteString("
")
}
pd.Title = repoName + " — files"
pd.GitCmd = cmd
pd.Body = template.HTML(body.String())
utils.RenderPage(c, pd)
}
}
// GET /:repo/refs
func HandleRefs(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repoName, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
pd := utils.BasePageData(c, repoName, rp, "refs",
template.HTML(fmt.Sprintf(
`repos / %s / refs`,
url.PathEscape(repoName), template.HTMLEscapeString(repoName),
)))
refs, cmd, err := git.ListRefs(rp)
var body strings.Builder
body.WriteString(`Refs
Branches and tags in this repository
`)
if err != nil || len(refs) == 0 {
body.WriteString(`No refs found.
`)
} else {
body.WriteString(` | Name | Type | Hash |
`)
for _, r := range refs {
badgeClass, badgeLabel, icon := "ref-branch", "branch", "⎇"
if r.Kind == "tag" {
badgeClass, badgeLabel, icon = "ref-tag", "tag", "🏷"
}
body.WriteString(fmt.Sprintf(
`| %s | %s | `+
`%s | `+
`%s |
`,
icon, template.HTMLEscapeString(r.Name),
badgeClass, badgeLabel, template.HTMLEscapeString(r.Short),
))
}
body.WriteString("
")
}
pd.Title = repoName + " — refs"
pd.GitCmd = cmd
pd.Body = template.HTML(body.String())
utils.RenderPage(c, pd)
}
}
// GET /:repo/log
func HandleLog(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repoName, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
pd := utils.BasePageData(c, repoName, rp, "log",
template.HTML(fmt.Sprintf(
`repos / %s / log`,
url.PathEscape(repoName), template.HTMLEscapeString(repoName),
)))
commits, cmd, err := git.ListLog(rp, pd.Branch, 200)
var body strings.Builder
body.WriteString(fmt.Sprintf(
`Log
`+
`branch: %s · %d commits shown
`,
template.HTMLEscapeString(pd.Branch), len(commits),
))
if err != nil || len(commits) == 0 {
body.WriteString(`No commits found.
`)
} else {
body.WriteString(``)
for _, cm := range commits {
body.WriteString(fmt.Sprintf(
`
`+
`
%s`+
`
%s`+
`
%s`+
`
%s`+
`
`,
url.PathEscape(repoName),
url.QueryEscape(cm.Hash), url.QueryEscape(pd.Branch),
template.HTMLEscapeString(cm.Short),
template.HTMLEscapeString(cm.Message),
template.HTMLEscapeString(cm.Author),
template.HTMLEscapeString(cm.Date),
))
}
body.WriteString("
")
}
pd.Title = repoName + " — log"
pd.GitCmd = cmd
pd.Body = template.HTML(body.String())
utils.RenderPage(c, pd)
}
}
// GET /:repo/history?path=&branch=
func HandleHistory(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repoName, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
path := c.Query("path")
if path == "" {
c.Redirect(http.StatusFound, "/"+repoName+"/")
return
}
pd := utils.BasePageData(c, repoName, rp, "files",
template.HTML(fmt.Sprintf(
`repos / %s / %s`,
url.PathEscape(repoName), template.HTMLEscapeString(repoName),
template.HTMLEscapeString(path),
)))
commits, cmd, err := git.FileHistory(rp, path, pd.Branch)
var body strings.Builder
body.WriteString(fmt.Sprintf(`← all files`,
url.PathEscape(repoName), url.QueryEscape(pd.Branch)))
body.WriteString(fmt.Sprintf(
`%s
`+
`Version history · %d commits
`,
template.HTMLEscapeString(path), len(commits),
))
if err != nil || len(commits) == 0 {
body.WriteString(`No history found for this file.
`)
} else {
body.WriteString(``)
for _, cm := range commits {
body.WriteString(fmt.Sprintf(
`
`+
`
%s`+
`
%s`+
`
%s`+
`
%s`+
`
`,
url.PathEscape(repoName),
url.QueryEscape(path), url.QueryEscape(cm.Hash), url.QueryEscape(pd.Branch),
template.HTMLEscapeString(cm.Short),
template.HTMLEscapeString(cm.Message),
template.HTMLEscapeString(cm.Author),
template.HTMLEscapeString(cm.Date),
))
}
body.WriteString("
")
}
pd.Title = path + " — history"
pd.GitCmd = cmd
pd.Body = template.HTML(body.String())
utils.RenderPage(c, pd)
}
}
// GET /:repo/view?path=&hash=&branch=
func HandleView(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repoName, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
path := c.Query("path")
hash := c.Query("hash")
if path == "" || hash == "" {
c.Redirect(http.StatusFound, "/"+repoName+"/")
return
}
short := hash
if len(short) > 8 {
short = short[:8]
}
pd := utils.BasePageData(c, repoName, rp, "files",
template.HTML(fmt.Sprintf(
`repos / %s`+
` / %s`+
` / %s`,
url.PathEscape(repoName), template.HTMLEscapeString(repoName),
url.PathEscape(repoName), url.QueryEscape(path), url.QueryEscape(utils.ActiveBranch(c, "")),
template.HTMLEscapeString(path), template.HTMLEscapeString(short),
)))
content, cmd, err := git.FileAtCommit(rp, hash, path)
var body strings.Builder
body.WriteString(fmt.Sprintf(
`← history for %s`,
url.PathEscape(repoName), url.QueryEscape(path), url.QueryEscape(pd.Branch),
template.HTMLEscapeString(path),
))
if err != nil {
body.WriteString(fmt.Sprintf(
`⚠ Could not retrieve file at %s: %s
`,
template.HTMLEscapeString(short), template.HTMLEscapeString(err.Error()),
))
} else {
lines := strings.Split(content, "\n")
rawURL := fmt.Sprintf("/%s/raw?path=%s&hash=%s", url.PathEscape(repoName), url.QueryEscape(path), url.QueryEscape(hash))
dlURL := rawURL + "&dl=1"
filename := path[strings.LastIndex(path, "/")+1:]
rows, chromaCSS := git.HighlightCode(content, filename)
body.WriteString(fmt.Sprintf(
``+
``,
template.HTMLEscapeString(short),
template.HTMLEscapeString(path),
len(lines), rawURL, dlURL,
template.HTMLEscapeString(filename),
))
body.WriteString(rows)
body.WriteString("
")
pd.ChromaCSS = template.CSS(chromaCSS)
}
pd.Title = fmt.Sprintf("%s @ %s — %s", path, short, repoName)
pd.GitCmd = cmd
pd.Body = template.HTML(body.String())
utils.RenderPage(c, pd)
}
}
// GET /:repo/raw?path=&hash=[&dl=1]
func HandleRaw(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
_, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
path := c.Query("path")
hash := c.Query("hash")
if path == "" || hash == "" {
c.String(http.StatusBadRequest, "missing path or hash")
return
}
content, _, err := git.FileAtCommit(rp, hash, path)
if err != nil {
c.String(http.StatusNotFound, "file not found at that revision: %s", err.Error())
return
}
ct := "text/plain; charset=utf-8"
lower := strings.ToLower(path)
switch {
case strings.HasSuffix(lower, ".html") || strings.HasSuffix(lower, ".htm"):
ct = "text/html; charset=utf-8"
case strings.HasSuffix(lower, ".json"):
ct = "application/json"
case strings.HasSuffix(lower, ".xml"):
ct = "application/xml"
case strings.HasSuffix(lower, ".css"):
ct = "text/css; charset=utf-8"
case strings.HasSuffix(lower, ".js") || strings.HasSuffix(lower, ".ts"):
ct = "text/javascript; charset=utf-8"
}
c.Header("Content-Type", ct)
if c.Query("dl") == "1" {
filename := path[strings.LastIndex(path, "/")+1:]
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
}
c.String(http.StatusOK, content)
}
}
// GET /:repo/commit?hash=&branch=
func HandleCommit(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc {
return func(c *gin.Context) {
repoName, rp, ok := utils.ResolveRepo(cfg, c.Param("repo"))
if !ok {
return
}
hash := c.Query("hash")
if hash == "" {
c.Redirect(http.StatusFound, "/"+repoName+"/log")
return
}
short := hash
if len(short) > 8 {
short = short[:8]
}
pd := utils.BasePageData(c, repoName, rp, "log",
template.HTML(fmt.Sprintf(
`repos / %s`+
` / log`+
` / %s`,
url.PathEscape(repoName), template.HTMLEscapeString(repoName),
url.PathEscape(repoName), url.QueryEscape(utils.ActiveBranch(c, "")),
template.HTMLEscapeString(short),
)))
meta, cmd := git.CommitMeta(rp, hash)
filesOut := git.CommitFiles(rp, hash)
var subject, author, email, date string
parts := strings.SplitN(meta, "\x1f", 4)
if len(parts) == 4 {
subject, author, email, date = parts[0], parts[1], parts[2], parts[3]
}
var body strings.Builder
body.WriteString(fmt.Sprintf(`← log`,
url.PathEscape(repoName), url.QueryEscape(pd.Branch)))
body.WriteString(fmt.Sprintf(
`%s
`+
`%s · %s <%s> · %s
`,
template.HTMLEscapeString(subject), template.HTMLEscapeString(short),
template.HTMLEscapeString(author), template.HTMLEscapeString(email),
template.HTMLEscapeString(date),
))
if filesOut == "" {
body.WriteString(`No file changes recorded.
`)
} else {
body.WriteString(`| Status | File |
`)
for _, line := range strings.Split(filesOut, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
f := strings.Fields(line)
if len(f) < 2 {
continue
}
status, fp := f[0], f[1]
color, label := "var(--text)", status
switch {
case strings.HasPrefix(status, "A"):
color, label = "var(--green)", "added"
case strings.HasPrefix(status, "D"):
color, label = "var(--red)", "deleted"
case strings.HasPrefix(status, "M"):
color, label = "var(--accent)", "modified"
case strings.HasPrefix(status, "R"):
color, label = "var(--gold)", "renamed"
}
body.WriteString(fmt.Sprintf(
`| %s | `+
`%s |
`,
color, label,
url.PathEscape(repoName), url.QueryEscape(fp), url.QueryEscape(pd.Branch),
template.HTMLEscapeString(fp),
))
}
body.WriteString("
")
}
pd.Title = short + " — " + repoName
pd.GitCmd = cmd
pd.Body = template.HTML(body.String())
utils.RenderPage(c, pd)
}
}