Нет описания

quit.go 9.2KB

    package main import ( "bytes" "html/template" "io" "log" "net/http" "os" "strings" "time" "github.com/libgit2/git2go" "github.com/russross/blackfriday" ) func main() { repoPath := "." if len(os.Args) > 1 { repoPath = os.Args[1] } repo, err := git.OpenRepository(repoPath) if err != nil { log.Fatal("opening repository: ", err) } http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { buf := new(bytes.Buffer) err = repoTmpl.Execute(buf, map[string]interface{}{ "RepoPath": repoPath, "Repo": NewFancyRepo(repo), "Style": template.CSS(repoStyle), }) if err != nil { log.Println("rendering repo: ", err) http.Error(w, "internal server error", http.StatusInternalServerError) return } io.Copy(w, buf) }) log.Fatal(http.ListenAndServe("localhost:12345", nil)) } type FancyRepo struct { repo *git.Repository commit *FancyCommit commitCount int branches []*git.Branch contributors []*git.Signature } 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() } 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() time.Time { return fc.commit.Author().When } 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() } func (fc *FancyCommit) Files() ([]*FancyFile, error) { if fc.files == nil { tree, err := fc.commit.Tree() if err != nil { return nil, err } err = tree.Walk(func(s string, entry *git.TreeEntry) int { fc.files = append(fc.files, NewFancyFile(fc.commit.Owner(), entry)) return 1 }) if err != nil { return nil, err } } 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 { commit, err := fc.repo.LookupCommit(fc.entry.Id) if err != nil { return nil, err } fc.commit = NewFancyCommit(commit) } 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 } var repoTmpl = template.Must(template.New("").Parse(`<!doctype html> <html> <head> <meta charset="utf-8" /> <title>{{ .RepoPath }}</title> <style>{{ .Style }}</style> </head> <body> <section id="repo" class="repo-info"> <a id="commits" class="repo-stat" href="#"><span class="icon">⏲</span><span class="count">{{ .Repo.CommitCount }}</span> commits</a> <a id="branches" class="repo-stat" href="#"><span class="icon">⛗</span><span class="count">{{ len .Repo.Branches }}</span> branches</a> <a id="tags" class="repo-stat" href="#"><span class="icon">🏷&#xFE0E;</span><span class="count">{{ len .Repo.Tags }}</span> tags</a> <a id="contributors" class="repo-stat" href="#"><span class="icon">👥</span><span class="count">{{ len .Repo.Contributors }}</span> contributors</a> </section> <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">{{ .Repo.Commit.Date }}</time> </section> <table id="files"> {{ range $file := .Repo.Commit.Files }} <tr class="file"> <td class="file-name file-type-{{ $file.Type }}">{{ $file.Name }}</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 }} </body> </html> `)) var repoStyle = ` * { box-sizing: border-box; } body { font-family: Arial, sans-serif; } #repo { display: flex; justify-content: center; width: 70vw; border: 1px solid #ddd; 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; } #commit { display: flex; justify-content: space-between; align-items: baseline; padding: 0.6em 0.5em; width: 70vw; 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; } #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 { width: 70vw; } #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; } `