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("")
}
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("%d %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(`
")
template.HTMLEscape(buf, contents)
buf.WriteString("
| {{ $file.Name }} | {{ $file.Commit.Summary }} | {{ $file.Commit.Date.PrettyString }} |