bc57e9101744c6393d52f1a60c71c206d29347dd / internal/web/repohome.go · 6927 bytes · raw
package web
import (
"html/template"
"net/http"
"net/url"
"path"
"strings"
"git.josie-c.com/josie/simplegit/internal/db"
"git.josie-c.com/josie/simplegit/internal/git"
"git.josie-c.com/josie/simplegit/internal/render"
)
// readmeCandidates are tried in order at the repo root; first hit wins.
// Matched case-insensitively (a lowercase readme.md is just as valid as
// README.md), so candidates stay lowercase for comparison.
var readmeCandidates = []string{"readme.md", "readme.markdown", "readme.mkd", "readme"}
const (
readmeLimit = 1 << 20
licenseLimit = 64 << 10
)
// treeNode is one entry in the Code tab's file tree: a directory (Children,
// no Href) or a file (Href to its blob view).
type treeNode struct {
Name string
Href string
Children []*treeNode
}
// buildFileTree turns a recursive path list into nested nodes; directories
// materialize on demand in git's leaf order.
func buildFileTree(paths []string, href func(string) string) []*treeNode {
var roots []*treeNode
dirs := map[string]*treeNode{}
for _, p := range paths {
parts := strings.Split(p, "/")
parent := &roots
dirPath := ""
for i, part := range parts {
if i == len(parts)-1 {
*parent = append(*parent, &treeNode{Name: part, Href: href(p)})
continue
}
if dirPath != "" {
dirPath += "/"
}
dirPath += part
node, ok := dirs[dirPath]
if !ok {
node = &treeNode{Name: part + "/"}
dirs[dirPath] = node
*parent = append(*parent, node)
}
parent = &node.Children
}
}
return roots
}
// buildTree lists ref's files and nests them for the Code tab sidebar. A
// listing failure (empty or unreadable repo) just leaves the tree empty.
func (s *Server) buildTree(r *http.Request, repo db.Repo, ref string) []*treeNode {
paths, err := git.LsTreePaths(s.repoPathFor(r.PathValue("user"), repo), ref)
if err != nil {
return nil
}
hrefBase := "/" + r.PathValue("user") + "/" + repo.Name + "/blob/" + url.PathEscape(ref) + "/"
return buildFileTree(paths, func(p string) string { return hrefBase + escapePath(p) })
}
type repoHomeData struct {
navData
Description string
Branch string // the ref being viewed
DefaultBranch string
CloneURL string
Empty bool
Tree []*treeNode
Readme template.HTML
LicenseName string
LicenseHref string
CommitCount int
BranchCount int
TagCount int
Branches []string
}
func (s *Server) baseRepoData(r *http.Request, repo db.Repo, user *db.User) repoHomeData {
return repoHomeData{
navData: nav(r, repo, user, "code"),
Description: repo.Description,
Branch: repo.DefaultBranch,
DefaultBranch: repo.DefaultBranch,
CloneURL: cloneURL(s.cfg.BaseURL, r.PathValue("user"), repo.Name),
}
}
func (s *Server) handleRepoHome(w http.ResponseWriter, r *http.Request) {
repo, repoPath, user, ok := s.resolveRepo(w, r)
if !ok {
return
}
data := s.baseRepoData(r, repo, user)
exists, err := git.RefExists(repoPath, repo.DefaultBranch)
if err != nil {
s.internalError(w, r, err)
return
}
if !exists {
data.Empty = true
s.render(w, "repo.html", http.StatusOK, data)
return
}
entries, err := git.LsTree(repoPath, repo.DefaultBranch, "")
if err != nil {
s.internalError(w, r, err)
return
}
s.renderCodeTab(w, r, repo, repoPath, user, repo.DefaultBranch, entries)
}
// attachTree builds the recursive file tree for the Code tab.
func (s *Server) attachTree(data *repoHomeData, repoPath string) {
paths, err := git.LsTreePaths(repoPath, data.Branch)
if err != nil {
return
}
base := "/" + data.Owner + "/" + data.RepoName + "/blob/" + url.PathEscape(data.Branch) + "/"
data.Tree = buildFileTree(paths, func(p string) string { return base + escapePath(p) })
}
// attachRepoMeta fills the Code tab's summary: commit/branch/tag counts and
// the branch list for the dropdown.
func (s *Server) attachRepoMeta(data *repoHomeData, repoPath string) {
if n, err := git.CommitCount(repoPath, data.Branch); err == nil {
data.CommitCount = n
}
if branches, err := git.Branches(repoPath); err == nil {
data.Branches = branches
data.BranchCount = len(branches)
}
if tags, err := git.Tags(repoPath); err == nil {
data.TagCount = len(tags)
}
}
// attachReadme renders the first README candidate at the repo root.
func (s *Server) attachReadme(data *repoHomeData, repoPath string, entries []git.TreeEntry) {
var readme string
for _, candidate := range readmeCandidates {
for _, entry := range entries {
if entry.Type == "blob" && strings.EqualFold(entry.Path, candidate) {
readme = entry.Path
break
}
}
if readme != "" {
break
}
}
if readme == "" {
return
}
source, err := git.ShowFile(repoPath, data.Branch, readme, readmeLimit)
if err != nil || len(source) > readmeLimit {
return
}
if strings.HasPrefix(strings.ToLower(readme), "readme.") {
html, err := render.Markdown(source)
if err != nil {
return
}
data.Readme = template.HTML(html)
return
}
data.Readme = template.HTML("<pre>" + template.HTMLEscapeString(string(source)) + "</pre>")
}
// attachLicense sniffs the first LICENSE*/COPYING* file at the repo root and
// links the license summary box to it.
func (s *Server) attachLicense(data *repoHomeData, repoPath string, entries []git.TreeEntry) {
for _, entry := range entries {
base := strings.ToUpper(path.Base(entry.Path))
if entry.Type != "blob" || !strings.HasPrefix(base, "LICENSE") && !strings.HasPrefix(base, "COPYING") {
continue
}
source, err := git.ShowFile(repoPath, data.Branch, entry.Path, licenseLimit)
if err != nil {
return
}
if len(source) > licenseLimit {
source = source[:licenseLimit]
}
data.LicenseName = detectLicense(string(source))
data.LicenseHref = "/" + data.Owner + "/" + data.RepoName + "/blob/" + escapePath(data.Branch+"/"+entry.Path)
return
}
}
// detectLicense is a deliberately dumb keyword sniff over the license
// text; anything unrecognised but present is "Custom".
func detectLicense(text string) string {
upper := strings.ToUpper(text)
switch {
case strings.Contains(upper, "MIT LICENSE"),
strings.Contains(upper, "PERMISSION IS HEREBY GRANTED, FREE OF CHARGE"):
return "MIT"
case strings.Contains(upper, "APACHE LICENSE, VERSION 2"):
return "Apache-2.0"
case strings.Contains(upper, "BSD 3-CLAUSE"), strings.Contains(upper, "BSD 3. CLAUSE"):
return "BSD-3-Clause"
case strings.Contains(upper, "BSD 2-CLAUSE"), strings.Contains(upper, "BSD 2. CLAUSE"):
return "BSD-2-Clause"
case strings.Contains(upper, "ISC LICENSE"):
return "ISC"
case strings.Contains(upper, "UNLICENSE"):
return "Unlicense"
case strings.Contains(upper, "GNU GENERAL PUBLIC LICENSE"):
return "GPL"
case strings.Contains(upper, "GNU LESSER GENERAL PUBLIC LICENSE"):
return "LGPL"
case strings.Contains(upper, "MOZILLA PUBLIC LICENSE"):
return "MPL"
default:
return "Custom"
}
}