Няма описание

quit.go 18KB

    package main import ( "bytes" "fmt" "html/template" "io" "log" "math" "net/http" "os" "path" "path/filepath" "sort" "strings" "sync" "time" "github.com/libgit2/git2go" "github.com/russross/blackfriday" "github.com/heyLu/quit/perf" ) var performance = perf.NewPerf() func main() { repoPath := "." if len(os.Args) > 1 { repoPath = os.Args[1] } var err error repoPath, err = filepath.Abs(repoPath) if err != nil { log.Fatal("getting absolute path: ", err) } repo, err := git.OpenRepository(repoPath) if err != nil { log.Fatal("opening repository: ", err) } http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { performance.MeasureRequest(req) handleError := func(w http.ResponseWriter, err error) { log.Println("git: ", err) http.Error(w, "internal server error", http.StatusInternalServerError) } fancyRepo := NewFancyRepo(repo) commit, err := fancyRepo.Commit() if err != nil { handleError(w, err) return } files, err := commit.Files(false) if err != nil { handleError(w, err) return } var wg sync.WaitGroup wg.Add(len(files)) for _, f := range files { go func(f *FancyFile) { defer wg.Done() _, err := f.Commit() if err != nil { handleError(w, err) } }(f) } wg.Add(2) go func() { defer wg.Done() _, err := fancyRepo.CommitCount() if err != nil { handleError(w, err) } }() go func() { defer wg.Done() _, err := fancyRepo.Contributors() if err != nil { handleError(w, err) } }() wg.Wait() buf := new(bytes.Buffer) start := time.Now() err = repoTmpl.Execute(buf, map[string]interface{}{ "RepoPath": path.Base(repoPath), "Repo": fancyRepo, "Style": template.CSS(repoStyle), "Performance": performance.Render(req), }) performance.Measure(req, "template", time.Since(start).String()) if err != nil { log.Println("rendering repo: ", err) http.Error(w, "internal server error", http.StatusInternalServerError) return } io.Copy(w, buf) }) http.Handle("/__performance__/", performance) log.Fatal(http.ListenAndServe("localhost:12345", nil)) } type FancyRepo struct { repo *git.Repository commit *FancyCommit commitCount int branches []*git.Branch contributors []*git.Signature languageStats *LanguageStats } func NewFancyRepo(repo *git.Repository) *FancyRepo { return &FancyRepo{repo: repo, commitCount: -1} } func (fr *FancyRepo) Commit() (*FancyCommit, error) { if fr.commit == nil { head, err := fr.repo.Head() if err != nil { return nil, err } commit, err := fr.repo.LookupCommit(head.Target()) if err != nil { return nil, err } fr.commit = NewFancyCommit(commit) } return fr.commit, nil } func (fr *FancyRepo) CommitCount() (int, error) { if fr.commitCount < 0 { walk, err := fr.repo.Walk() if err != nil { return -1, err } head, err := fr.repo.Head() if err != nil { return -1, err } err = walk.Push(head.Target()) if err != nil { return -1, err } c := 0 err = walk.Iterate(func(commit *git.Commit) bool { c += 1 return true }) if err != nil { return -1, err } fr.commitCount = c } return fr.commitCount, nil } func (fr *FancyRepo) Contributors() ([]*git.Signature, error) { if fr.contributors == nil { walk, err := fr.repo.Walk() if err != nil { return nil, err } head, err := fr.repo.Head() if err != nil { return nil, err } err = walk.Push(head.Target()) if err != nil { return nil, err } authors := make(map[string]*git.Signature) err = walk.Iterate(func(commit *git.Commit) bool { authors[commit.Author().Name] = commit.Author() return true }) if err != nil { return nil, err } for _, author := range authors { fr.contributors = append(fr.contributors, author) } } return fr.contributors, nil } func (fr *FancyRepo) Branches() ([]*git.Branch, error) { if fr.branches == nil { it, err := fr.repo.NewBranchIterator(git.BranchRemote) if err != nil { return nil, err } err = it.ForEach(func(b *git.Branch, bt git.BranchType) error { fr.branches = append(fr.branches, b) return nil }) if err != nil { return nil, err } } return fr.branches, nil } func (fr *FancyRepo) Tags() ([]string, error) { return fr.repo.Tags.List() } var languageSuffixes = map[string]string{ ".c": "C", ".h": "C", ".cpp": "C++", "CMakeLists.txt": "CMake", ".cmake": "CMake", ".css": "CSS", ".go": "Go", ".html": "HTML", ".js": "JavaScript", ".java": "Java", "Makefile": "Makefile", ".py": "Python", ".rb": "Ruby", ".rs": "Rust", ".sh": "Shell", ".bash": "Shell", } var languageColors = map[string]string{ "C": "#555555", "C++": "#f34b7d", //"CMake": "", "CSS": "#563d7c", "Go": "#375eab", "HTML": "#e34c26", "JavaScript": "#f1e05a", "Java": "#b07219", "Makefile": "#427819", "Python": "#3572A5", "Ruby": "#701516", "Rust": "#dea584", "Shell": "#89e051", } func fileToLanguage(filename string) string { filename = strings.ToLower(filename) for suffix, lang := range languageSuffixes { if strings.HasSuffix(filename, suffix) { return lang } } return "" } type LanguageStats struct { percentages map[string]float64 Languages []*LanguageInfo } type LanguageInfo struct { Language string Percentage float64 } func (li *LanguageInfo) String() string { return fmt.Sprintf("%s (%.2f%%)", li.Language, li.Percentage) } type languageByPercent []*LanguageInfo func (l languageByPercent) Len() int { return len(l) } func (l languageByPercent) Less(i, j int) bool { return l[i].Percentage < l[j].Percentage } func (l languageByPercent) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func NewLanguageStats(percentages map[string]float64) *LanguageStats { for lang, percentage := range percentages { if percentage < 0.001 { delete(percentages, lang) continue } percentage = math.Trunc(percentage*10000) / 100 percentages[lang] = percentage } languages := make([]*LanguageInfo, 0, len(percentages)) for lang, percentage := range percentages { languages = append(languages, &LanguageInfo{Language: lang, Percentage: percentage}) } sort.Sort(sort.Reverse(languageByPercent(languages))) return &LanguageStats{ percentages: percentages, Languages: languages, } } func (li *LanguageStats) String() string { return fmt.Sprintf("%v", li.percentages) } func (fr *FancyRepo) LanguageStats() (*LanguageStats, error) { if fr.languageStats == nil { commit, err := fr.Commit() if err != nil { return nil, err } files, err := commit.Files(true) if err != nil { return nil, err } total := 0 languages := make(map[string]int) for _, f := range files { if f.entry.Type != git.ObjectBlob { continue } lang := fileToLanguage(f.Name()) if lang == "" { continue } total += 1 languages[lang] += 1 } percentages := make(map[string]float64) for lang, count := range languages { percentages[lang] = float64(count) / float64(total) } fr.languageStats = NewLanguageStats(percentages) } return fr.languageStats, nil } type FancyCommit struct { commit *git.Commit files []*FancyFile filesCache map[string]*FancyFile } func NewFancyCommit(commit *git.Commit) *FancyCommit { return &FancyCommit{ commit: commit, filesCache: make(map[string]*FancyFile), } } func (fc *FancyCommit) Author() string { return fc.commit.Author().Name } func (fc *FancyCommit) Date() *FancyTime { return &FancyTime{fc.commit.Author().When} } type FancyTime struct { time.Time } func (fc FancyTime) PrettyString() string { makeForm := func(c int, singular, plural string) string { form := plural if c == 1 { form = singular } return fmt.Sprintf("%d %s ago", c, form) } now := time.Now() diff := time.Since(fc.Time) switch { case diff < time.Minute: return makeForm(int(diff/time.Second), "second", "seconds") case diff < time.Hour: return makeForm(int(diff/time.Minute), "minute", "minutes") case now.AddDate(0, 0, -1).Before(fc.Time): return makeForm(int(diff/time.Hour), "hour", "hours") case now.AddDate(0, 0, -7).Before(fc.Time): d := now.Day() - fc.Time.Day() if d <= 0 { d += 30 } return makeForm(d, "day", "days") case now.AddDate(0, -1, 0).Before(fc.Time): d := now.Day() - fc.Time.Day() if d <= 0 { d += 30 } return makeForm(d/7, "week", "weeks") case now.AddDate(-1, 0, 0).Before(fc.Time): m := now.Month() - fc.Time.Month() if m <= 0 { m += 12 } return makeForm(int(m), "month", "months") default: return makeForm(now.Year()-fc.Time.Year(), "year", "years") } } func (fc *FancyCommit) Id(n int) string { if n == 0 { return fc.commit.TreeId().String() } return fc.commit.TreeId().String()[:n] } func (fc *FancyCommit) Summary() string { return fc.commit.Summary() } func (fc *FancyCommit) Description() string { return fc.commit.Message() } type byTypeAndName []*FancyFile func (b byTypeAndName) Len() int { return len(b) } func (b byTypeAndName) Less(i, j int) bool { return b[i].Less(b[j]) } func (b byTypeAndName) Swap(i, j int) { b[i], b[j] = b[j], b[i] } func (fc *FancyCommit) Files(recursive bool) ([]*FancyFile, error) { if recursive || fc.files == nil { tree, err := fc.commit.Tree() if err != nil { return nil, err } recurse := 1 if recursive { recurse = 0 } files := make([]*FancyFile, 0) err = tree.Walk(func(s string, entry *git.TreeEntry) int { files = append(files, NewFancyFile(fc.commit.Owner(), entry)) return recurse }) if err != nil { return nil, err } sort.Sort(byTypeAndName(files)) if recursive { return files, nil } fc.files = files } return fc.files, nil } var readmeNames = []string{"README.md", "readme.md", "README.rst", "readme.rst", "README.txt", "readme.txt", "README"} func (fc *FancyCommit) Readme() (*FancyFile, error) { readme := fc.filesCache["|||README|||"] if readme == nil { tree, err := fc.commit.Tree() if err != nil { return nil, err } var entry *git.TreeEntry for _, n := range readmeNames { entry, err = tree.EntryByPath(n) if err == nil { break } } if entry == nil { return nil, nil } readme = NewFancyFile(fc.commit.Owner(), entry) fc.filesCache["|||README|||"] = readme } return readme, nil } type FancyFile struct { repo *git.Repository entry *git.TreeEntry commit *FancyCommit } func NewFancyFile(repo *git.Repository, entry *git.TreeEntry) *FancyFile { return &FancyFile{repo: repo, entry: entry} } func (fc *FancyFile) Name() string { return fc.entry.Name } func (fc *FancyFile) Commit() (*FancyCommit, error) { if fc.commit == nil { walk, err := fc.repo.Walk() if err != nil { return nil, err } err = walk.PushHead() if err != nil { return nil, err } opts, err := git.DefaultDiffOptions() if err != nil { return nil, err } opts.Pathspec = []string{fc.entry.Name} var lastCommit *git.Commit var findErr error c := 0 err = walk.Iterate(func(commit *git.Commit) bool { c += 1 if commit.ParentCount() <= 0 { lastCommit = commit return false } tree, err := commit.Tree() if err != nil { findErr = err return false } matchesAll := true for i := uint(0); i < commit.ParentCount(); i++ { parentCommit := commit.Parent(i) parentTree, err := parentCommit.Tree() if err != nil { findErr = err return false } diff, err := fc.repo.DiffTreeToTree(parentTree, tree, &opts) if err != nil { findErr = err return false } n, err := diff.NumDeltas() if err != nil { findErr = err return false } matchesAll = matchesAll && n > 0 } if !matchesAll { return true } lastCommit = commit return false }) if err != nil { return nil, err } if findErr != nil { return nil, err } if lastCommit == nil { return nil, fmt.Errorf("did not find commit for %q", fc.entry.Name) } fc.commit = NewFancyCommit(lastCommit) } return fc.commit, nil } func (fc *FancyFile) Type() string { return strings.ToLower(fc.entry.Type.String()) } func (fc *FancyFile) RawContents() (string, error) { blob, err := fc.repo.LookupBlob(fc.entry.Id) if err != nil { return "", err } return string(blob.Contents()), nil } func (fc *FancyFile) Contents() (template.HTML, error) { blob, err := fc.repo.LookupBlob(fc.entry.Id) if err != nil { return "", err } buf := new(bytes.Buffer) contents := blob.Contents() if strings.HasSuffix(fc.entry.Name, ".md") { buf.Write(blackfriday.MarkdownCommon(contents)) } else { buf.WriteString("<code><pre>") template.HTMLEscape(buf, contents) buf.WriteString("</pre></code>") } return template.HTML(buf.String()), nil } func (fc *FancyFile) Less(file *FancyFile) bool { if fc.entry.Type == file.entry.Type { return fc.entry.Name < file.entry.Name } return fc.entry.Type < file.entry.Type } var repoFuncs = template.FuncMap{ "count": func(count int, singular, plural string) template.HTML { form := plural if count == 1 { form = singular } return template.HTML(fmt.Sprintf("<span class=\"count\">%d</span> %s", count, form)) }, "languageToColor": func(l string) string { c, ok := languageColors[l] if !ok { return "black" } return c }, } var repoTmpl = template.Must(template.New("").Funcs(repoFuncs).Parse(`<!doctype html> <html> <head> <meta charset="utf-8" /> <title>{{ .RepoPath }}</title> <style>{{ .Style }}</style> </head> <body> <section id="performance-bar"></section> <div id="container"> <section id="repo" class="repo-info"> <a id="commits" class="repo-stat" href="#"><span class="icon">⏲</span>{{ count .Repo.CommitCount "commit" "commits" }}</a> <a id="branches" class="repo-stat" href="#"><span class="icon">⛗</span>{{ count (len .Repo.Branches) "branch" "branches" }}</a> <a id="tags" class="repo-stat" href="#"><span class="icon">🏷&#xFE0E;</span>{{ count (len .Repo.Tags) "tag" "tags" }}</a> <a id="contributors" class="repo-stat" href="#"><span class="icon">👥</span>{{ count (len .Repo.Contributors) "contributor" "contributors" }}</a> </section> <div id="languages"> <ol class="language-list"> {{ range $language := .Repo.LanguageStats.Languages }} <li> <span class="language">{{ $language.Language }}</span> <span class="percentage">{{ $language.Percentage }}%</span> </li> {{ end }} </ol> <div id="language-bar"> {{ range $language := .Repo.LanguageStats.Languages }} <span class="language" title="{{ $language }}" style="width: {{ $language.Percentage }}%; background-color: {{ languageToColor $language.Language }}"><!--{{ $language.Language }}--></span> {{ end }} </div> </div> <section id="commit" class="commit-info"> <div class="commit-author">{{ .Repo.Commit.Author }}</div> <div class="commit-summary" title="{{ .Repo.Commit.Description }}">{{ .Repo.Commit.Summary }}</div> <div class="commit-id" data-commit="{{ .Repo.Commit.Id 0 }}"><a href="#">{{ .Repo.Commit.Id 10 }}</a></div> <time class="commit-date" title="{{ .Repo.Commit.Date }}">{{ .Repo.Commit.Date.PrettyString }}</time> </section> <table id="files"> {{ range $file := .Repo.Commit.Files false }} <tr class="file"> <td class="file-name file-type-{{ $file.Type }}">{{ $file.Name }}</td> <td class="file-commit" title="{{ $file.Commit.Description }}">{{ $file.Commit.Summary }}</td> <td class="file-date" title="{{ $file.Commit.Date }}">{{ $file.Commit.Date.PrettyString }}</td> </tr> {{ end }} </table> {{ if .Repo.Commit.Readme }} <section id="file" class="readme"> <h1 class="file-name">{{ .Repo.Commit.Readme.Name }}</h1> <div class="contents"> {{ .Repo.Commit.Readme.Contents }} </div> </section> {{ end }} </div> {{ .Performance }} </body> </html> `)) var repoStyle = ` * { box-sizing: border-box; } body { font-family: Arial, sans-serif; } #container { width: 70vw; margin: 0 auto; } #repo { display: flex; justify-content: center; border: 1px solid #ddd; border-radius: 3px; padding: 0.75em; margin-bottom: 1em } #repo .repo-stat { margin-right: 1em; color: black; text-decoration: none; } #repo .repo-stat:hover { color: #ad56a0; } #repo .repo-stat:hover .icon { color: black; } #repo .repo-stat .count { font-weight: 600; } #repo .icon { padding-right: 0.2em; } .language-list { display: none; } #language-bar { display: flex; height: 0.5em; margin: -1em 0 1em; } #commit { display: flex; justify-content: space-between; align-items: baseline; padding: 0.6em 0.5em; background-color: hsla(300, 70%, 80%, 0.2); border: 1px solid rgba(200, 0, 255, 0.15); border-radius: 3px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } #commit .commit-author { font-weight: 600; } #commit .commit-id { font-family: monospace; } #files td { border: 1px solid rgba(200, 0, 200, 0.15); border-radius: 3px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; padding: 0.2em 0.5em; font-size: 95%; } #files tr:hover { background-color: #efefef; } #files { width: 70vw; border-collapse: collapse; margin-top: -1px; } #files .file-name { color: #ad56a0; } .file-type-blob:before { content:"🖹"; padding-right: 0.5em; } .file-type-tree:before { content: "🗀"; padding-right: 0.2em; } .file-type-tree:after { content: "/"; } #file .file-name { font-size: 90%; padding: 0.5em; margin-bottom: -1px; background-color: hsla(300, 70%, 80%, 0.2); border: 1px solid #fbd3f5; border-radius: 3px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } #file .contents { border: 1px solid #fbd3f5; padding: 0.5em 3em; } #file .contents > h1 { border-bottom: 1px solid #ddd; padding-bottom: 0.5em; } `