|
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">🏷︎</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;
}
`
|