package routes import ( "fmt" "html/template" "net/http" "net/url" "repobrowser/internal/config" "repobrowser/internal/gitrepo" "repobrowser/internal/rendering" "strings" "github.com/gin-gonic/gin" ) // HandleRepoList GET / — list all repos func HandleRepoList(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repos := gitrepo.ListRepos(cfg) var body strings.Builder body.WriteString(fmt.Sprintf( `

Repositories

`+ `

%d repositories

`, 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("
") } rendering.RenderPage(c, rendering.PageData{ Title: "altgit", Breadcrumb: template.HTML(`repositories`), //GitCmd: fmt.Sprintf("ls %s", cfg.Root), GitCmd: fmt.Sprintf("ls "), // Do not show the root filepath for security reasons Body: template.HTML(body.String()), }) } } // HandleIndex GET /:repo/ — file list func HandleIndex(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repoName, rp, ok := rendering.ResolveRepo(cfg, c.Param("repo")) if !ok { return } pd := rendering.BasePageData(c, repoName, rp, "files", template.HTML(fmt.Sprintf( `repos / %s`, url.PathEscape(repoName), template.HTMLEscapeString(repoName), ))) files, cmd, err := gitrepo.ListFiles(rp, pd.Branch) var body strings.Builder body.WriteString(fmt.Sprintf( `

%s

`+ `

branch: %s  ·  %d files

`, template.HTMLEscapeString(repoName), template.HTMLEscapeString(pd.Branch), len(files), )) if err != nil || len(files) == 0 { body.WriteString(`
No tracked files found on this branch.
`) } else { body.WriteString(``) 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( ``+ ``, rendering.FileIcon(name), template.HTMLEscapeString(dir), url.PathEscape(repoName), url.QueryEscape(f.Path), url.QueryEscape(pd.Branch), template.HTMLEscapeString(name), f.CommitCount, )) } body.WriteString("
FileCommits
%s`+ `%s`+ `%s%d
") } pd.Title = repoName + " — files" pd.GitCmd = cmd pd.Body = template.HTML(body.String()) rendering.RenderPage(c, pd) } } // HandleRefs GET /:repo/refs func HandleRefs(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repoName, rp, ok := rendering.ResolveRepo(cfg, c.Param("repo")) if !ok { return } pd := rendering.BasePageData(c, repoName, rp, "refs", template.HTML(fmt.Sprintf( `repos / %s / refs`, url.PathEscape(repoName), template.HTMLEscapeString(repoName), ))) refs, cmd, err := gitrepo.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(``) for _, r := range refs { badgeClass, badgeLabel, icon := "ref-branch", "branch", "⎇" if r.Kind == "tag" { badgeClass, badgeLabel, icon = "ref-tag", "tag", "🏷" } body.WriteString(fmt.Sprintf( ``+ ``+ ``, icon, template.HTMLEscapeString(r.Name), badgeClass, badgeLabel, template.HTMLEscapeString(r.Short), )) } body.WriteString("
NameTypeHash
%s%s%s%s
") } pd.Title = repoName + " — refs" pd.GitCmd = cmd pd.Body = template.HTML(body.String()) rendering.RenderPage(c, pd) } } // HandleLog GET /:repo/log func HandleLog(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repoName, rp, ok := rendering.ResolveRepo(cfg, c.Param("repo")) if !ok { return } pd := rendering.BasePageData(c, repoName, rp, "log", template.HTML(fmt.Sprintf( `repos / %s / log`, url.PathEscape(repoName), template.HTMLEscapeString(repoName), ))) commits, cmd, err := gitrepo.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()) rendering.RenderPage(c, pd) } } // HandleHistory GET /:repo/history?path=&branch= func HandleHistory(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repoName, rp, ok := rendering.ResolveRepo(cfg, c.Param("repo")) if !ok { return } path := c.Query("path") if path == "" { c.Redirect(http.StatusFound, "/"+repoName+"/") return } pd := rendering.BasePageData(c, repoName, rp, "files", template.HTML(fmt.Sprintf( `repos / %s / %s`, url.PathEscape(repoName), template.HTMLEscapeString(repoName), template.HTMLEscapeString(path), ))) commits, cmd, err := gitrepo.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()) rendering.RenderPage(c, pd) } } // HandleView GET /:repo/view?path=&hash=&branch= func HandleView(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repoName, rp, ok := rendering.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 := rendering.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(rendering.ActiveBranch(c, "")), template.HTMLEscapeString(path), template.HTMLEscapeString(short), ))) content, cmd, err := gitrepo.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 := rendering.HighlightCode(content, filename) body.WriteString(fmt.Sprintf( `
`+ `%s`+ `%s`+ ``+ `%d lines`+ `⬡ raw`+ `↓ download`+ `
`+ `
`, 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()) rendering.RenderPage(c, pd) } } // HandleRaw GET /:repo/raw?path=&hash=[&dl=1] func HandleRaw(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { _, rp, ok := rendering.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 := gitrepo.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) } } // HandleCommit GET /:repo/commit?hash=&branch= func HandleCommit(cfg config.RepositoryBrowserConfiguration) gin.HandlerFunc { return func(c *gin.Context) { repoName, rp, ok := rendering.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 := rendering.BasePageData(c, repoName, rp, "log", template.HTML(fmt.Sprintf( `repos / %s`+ ` / log`+ ` / %s`, url.PathEscape(repoName), template.HTMLEscapeString(repoName), url.PathEscape(repoName), url.QueryEscape(rendering.ActiveBranch(c, "")), template.HTMLEscapeString(short), ))) meta, cmd := gitrepo.CommitMeta(rp, hash) filesOut := gitrepo.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(``) 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( ``+ ``, color, label, url.PathEscape(repoName), url.QueryEscape(fp), url.QueryEscape(pd.Branch), template.HTMLEscapeString(fp), )) } body.WriteString("
StatusFile
%s%s
") } pd.Title = short + " — " + repoName pd.GitCmd = cmd pd.Body = template.HTML(body.String()) rendering.RenderPage(c, pd) } }