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("
" + template.HTMLEscapeString(string(source)) + "
") } // 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" } }