--- /dev/null
+ SGBlog -- Git-based CGI blogging engine
+ =======================================
+
+DESCRIPTION
+
+SGBlog is a simple blogging engine with a Git-based storage and CGI
+interface with HTTP-server. It can be used as a cgit replacement for
+readonly Git repository viewing and Atom feed generation. But it has
+better features:
+
+* URLs are tried to be converted to clickable links
+* SHA1 hashes are tried to be converted to blog links itself
+* Relative <link rel> links are included for easy navigation
+* Each page has ETag and it is checked against the request for client-side
+ caching
+* Pages can be gzip-compressed, depending on Accept-Encoding header
+
+CONFIGURATION
+
+SGBlog is configured via environment variables:
+* SGBLOG_GIT_PATH points to .git directory you want to serve
+* SGBLOG_BRANCH points to the branch in it (refs/heads/master for example)
+* SGBLOG_BASE_URL points to full URL before the possible SGBLOG_URL_PREFIX
+* SGBLOG_TITLE sets the index title
+* SGBLOG_ATOM_ID sets Atom feed's id
+* SGBLOG_ATOM_AUTHOR sets Atom feed's author name
+* If SGBLOG_URL_PREFIX is set, then all link will be prefixed with that URL
+* If SGBLOG_CSS is set, then link to that CSS URL will be generated
+* If SGBLOG_WEBMASTER is set, then "made" link well be generated
+* If SGBLOG_ABOUT is set, then about link is generated at the bottom
+* If SGBLOG_GIT_URLS is set, then links to that vcs-git space-separated
+ URLs will be generated
+
+Example lighttpd's configuration:
+
+ setenv.add-environment = (
+ "SGBLOG_GIT_PATH" => "/home/git/pub/stargrave-blog.git",
+ "SGBLOG_BRANCH" => "refs/heads/russian",
+ "SGBLOG_BASE_URL" => "http://blog.stargrave.org",
+ "SGBLOG_TITLE" => "Russian Stargrave's blog",
+ "SGBLOG_ATOM_ID" => "urn:uuid:e803a056-1147-44d4-9332-5190cb78cc3d",
+ "SGBLOG_ATOM_AUTHOR" => "Sergey Matveev",
+ "SGBLOG_URL_PREFIX" => "/russian",
+ "SGBLOG_WEBMASTER" => "webmaster@stargrave.org",
+ "SGBLOG_CSS" => "http://blog.stargrave.org/style.css",
+ "SGBLOG_GIT_URLS" => "git://git.stargrave.org/stargrave-blog.git https://git.stargrave.org/git/stargrave-blog.git",
+ "SGBLOG_ABOUT" => "http://blog.stargrave.org/",
+ )
+
+LICENCE
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
--- /dev/null
+/*
+SGBlog -- Git-based CGI blogging engine
+Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// Git-based CGI blogging engine
+package main
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/hex"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/google/uuid"
+ "golang.org/x/crypto/blake2b"
+ "golang.org/x/tools/blog/atom"
+ "gopkg.in/src-d/go-git.v4"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+)
+
+const (
+ PageEntries = 50
+ WhenFmt = "2006-01-02 15:04:05Z07:00"
+ AtomFeed = "feed.atom"
+)
+
+var (
+ Version = "0.0.1"
+ ETagVersion = []byte("1")
+ sha1DigestRe = regexp.MustCompilePOSIX("([0-9a-f]{40,40})")
+ defaultLinks = []string{}
+ repo *git.Repository
+ commentsTree *object.Tree
+
+ renderableSchemes = map[string]struct{}{
+ "http": struct{}{},
+ "https": struct{}{},
+ "ftp": struct{}{},
+ "gopher": struct{}{},
+ }
+)
+
+func makeA(href, text string) string {
+ return fmt.Sprintf(`<a href="%s">%s</a>`, href, text)
+}
+
+func etagString(etag hash.Hash) string {
+ return `"` + hex.EncodeToString(etag.Sum(nil)) + `"`
+}
+
+func urlParse(what string) *url.URL {
+ if u, err := url.ParseRequestURI(what); err == nil {
+ if _, exists := renderableSchemes[u.Scheme]; exists {
+ return u
+ }
+ }
+ return nil
+}
+
+func msgSplit(msg string) []string {
+ lines := strings.Split(msg, "\n")
+ lines = lines[:len(lines)-1]
+ if len(lines) < 3 {
+ lines = []string{lines[0], "", ""}
+ }
+ return lines
+}
+
+func getNote(what plumbing.Hash) string {
+ if commentsTree == nil {
+ return ""
+ }
+ entry, err := commentsTree.FindEntry(what.String())
+ if err != nil {
+ return ""
+ }
+ blob, err := repo.BlobObject(entry.Hash)
+ if err != nil {
+ return ""
+ }
+ r, err := blob.Reader()
+ if err != nil {
+ return ""
+ }
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+func startHeader(etag hash.Hash, gziped bool) string {
+ lines := []string{
+ "Content-Type: text/html; charset=UTF-8",
+ "ETag: " + etagString(etag),
+ }
+ if gziped {
+ lines = append(lines, "Content-Encoding: gzip")
+ }
+ lines = append(lines, "")
+ lines = append(lines, "")
+ return strings.Join(lines, "\n")
+}
+
+func startHTML(title string, additional []string) string {
+ return fmt.Sprintf(`<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta name="generator" content="SGBlog %s">
+ <title>%s</title>
+ %s
+</head>
+<body>
+`,
+ Version, title,
+ strings.Join(append(defaultLinks, additional...), "\n "),
+ )
+}
+
+func makeErr(err error) {
+ fmt.Println("Content-Type: text/plain; charset=UTF-8\n")
+ fmt.Println(err)
+ panic(err)
+}
+
+func checkETag(etag hash.Hash) {
+ ifNoneMatch := os.Getenv("HTTP_IF_NONE_MATCH")
+ if ifNoneMatch != "" && ifNoneMatch == etagString(etag) {
+ fmt.Println("Status: 304\nETag:", ifNoneMatch, "\n")
+ os.Exit(0)
+ }
+}
+
+func main() {
+ gitPath, exists := os.LookupEnv("SGBLOG_GIT_PATH")
+ if !exists {
+ makeErr(errors.New("SGBLOG_GIT_PATH is unset"))
+ }
+ branchName, exists := os.LookupEnv("SGBLOG_BRANCH")
+ if !exists {
+ makeErr(errors.New("SGBLOG_BRANCH is unset"))
+ }
+ blogBaseURL, exists := os.LookupEnv("SGBLOG_BASE_URL")
+ if !exists {
+ makeErr(errors.New("SGBLOG_BASE_URL is unset"))
+ }
+ blogTitle, exists := os.LookupEnv("SGBLOG_TITLE")
+ if !exists {
+ makeErr(errors.New("SGBLOG_TITLE is unset"))
+ }
+ atomId, exists := os.LookupEnv("SGBLOG_ATOM_ID")
+ if !exists {
+ makeErr(errors.New("SGBLOG_ATOM_ID is unset"))
+ }
+ atomAuthorName, exists := os.LookupEnv("SGBLOG_ATOM_AUTHOR")
+ if !exists {
+ makeErr(errors.New("SGBLOG_ATOM_AUTHOR is unset"))
+ }
+
+ etagHash, err := blake2b.New256(nil)
+ if err != nil {
+ panic(err)
+ }
+ etagHash.Write(ETagVersion)
+ etagHash.Write([]byte(gitPath))
+ etagHash.Write([]byte(branchName))
+ etagHash.Write([]byte(blogBaseURL))
+ etagHash.Write([]byte(blogTitle))
+ etagHash.Write([]byte(atomId))
+ etagHash.Write([]byte(atomAuthorName))
+
+ // SGBLOG_URL_PREFIX
+ urlPrefix := os.Getenv("SGBLOG_URL_PREFIX")
+ etagHash.Write([]byte(urlPrefix))
+
+ // SGBLOG_CSS
+ if cssUrl, exists := os.LookupEnv("SGBLOG_CSS"); exists {
+ defaultLinks = append(defaultLinks, fmt.Sprintf(
+ `<link rel="stylesheet" type="text/css" href="%s">`,
+ cssUrl,
+ ))
+ etagHash.Write([]byte(cssUrl))
+ }
+
+ // SGBLOG_WEBMASTER
+ if webmaster, exists := os.LookupEnv("SGBLOG_WEBMASTER"); exists {
+ defaultLinks = append(defaultLinks, fmt.Sprintf(
+ `<link rev="made" href="mailto:%s">`,
+ webmaster,
+ ))
+ etagHash.Write([]byte(webmaster))
+ }
+
+ // SGBLOG_ABOUT
+ aboutUrl := os.Getenv("SGBLOG_ABOUT")
+ etagHash.Write([]byte(aboutUrl))
+
+ // SGBLOG_GIT_URLS
+ if gitUrls, exists := os.LookupEnv("SGBLOG_GIT_URLS"); exists {
+ for _, gitUrl := range strings.Split(gitUrls, " ") {
+ defaultLinks = append(defaultLinks, fmt.Sprintf(
+ `<link rel="vcs-git" href="%s" title="Git repository">`,
+ gitUrl,
+ ))
+ }
+ etagHash.Write([]byte(gitUrls))
+ }
+
+ defaultLinks = append(defaultLinks, fmt.Sprintf(
+ `<link rel="top" href="%s/" title="top">`,
+ urlPrefix,
+ ))
+ atomUrl := blogBaseURL + urlPrefix + "/" + AtomFeed
+ defaultLinks = append(defaultLinks, fmt.Sprintf(
+ `<link rel="alternate" title="Atom feed" href="%s" type="application/atom+xml">`,
+ atomUrl,
+ ))
+
+ pathInfo, exists := os.LookupEnv("PATH_INFO")
+ if !exists {
+ pathInfo = "/"
+ }
+ queryValues, err := url.ParseQuery(os.Getenv("QUERY_STRING"))
+ if err != nil {
+ makeErr(err)
+ }
+
+ repo, err = git.PlainOpen(gitPath)
+ if err != nil {
+ makeErr(err)
+ }
+ head, err := repo.Reference(plumbing.ReferenceName(branchName), false)
+ if err != nil {
+ makeErr(err)
+ }
+ if notes, err := repo.Notes(); err == nil {
+ var comments *plumbing.Reference
+ notes.ForEach(func(ref *plumbing.Reference) error {
+ if ref.Name() == "refs/notes/commits" {
+ comments = ref
+ }
+ return nil
+ })
+ if comments != nil {
+ if commentsCommit, err := repo.CommitObject(comments.Hash()); err == nil {
+ commentsTree, _ = commentsCommit.Tree()
+ }
+ }
+ }
+
+ var outBuf bytes.Buffer
+ var out io.Writer
+ out = &outBuf
+ var gzipWriter *gzip.Writer
+ acceptEncoding := os.Getenv("HTTP_ACCEPT_ENCODING")
+ for _, encoding := range strings.Split(acceptEncoding, ", ") {
+ if encoding == "gzip" {
+ gzipWriter = gzip.NewWriter(&outBuf)
+ out = gzipWriter
+ }
+ }
+
+ var commit *object.Commit
+ if pathInfo == "/" {
+ offset := 0
+ if offsetRaw, exists := queryValues["offset"]; exists {
+ offset, err = strconv.Atoi(offsetRaw[0])
+ if err != nil {
+ makeErr(err)
+ }
+ }
+ var table bytes.Buffer
+ table.WriteString("<table border=1>\n<tr><th>When</th><th>Title</th><th>Comment of</th></tr>\n")
+ log, err := repo.Log(&git.LogOptions{From: head.Hash()})
+ if err != nil {
+ makeErr(err)
+ }
+ errOccured := false
+ for i := 0; i < offset; i++ {
+ commit, err = log.Next()
+ if err != nil {
+ break
+ }
+ }
+ for i := 0; i < PageEntries; i++ {
+ commit, err = log.Next()
+ if err != nil {
+ errOccured = true
+ break
+ }
+ if i == 0 {
+ etagHash.Write(commit.Hash[:])
+ checkETag(etagHash)
+ }
+ lines := msgSplit(commit.Message)
+ domains := []string{}
+ for _, line := range lines[2:] {
+ if u := urlParse(line); u == nil {
+ break
+ } else {
+ domains = append(domains, makeA(line, u.Host))
+ }
+ }
+ entry := []string{
+ makeA(urlPrefix+"/"+commit.Hash.String(), lines[0]),
+ fmt.Sprintf("(%dL)", len(lines)-2),
+ }
+ if note := getNote(commit.Hash); note != "" {
+ entry = append(entry, "(N)")
+ }
+ table.WriteString(fmt.Sprintf(
+ "<tr><td><tt>%s</tt></td><td>%s</td><td>%s</td></tr>\n",
+ commit.Author.When.Format(WhenFmt),
+ strings.Join(entry, " "),
+ strings.Join(domains, " "),
+ ))
+ }
+ table.WriteString("</table>")
+ var links []string
+ var refs bytes.Buffer
+ if offset > 0 {
+ offsetPrev := offset - PageEntries
+ if offsetPrev < 0 {
+ offsetPrev = 0
+ }
+ href := urlPrefix + "/?offset=" + strconv.Itoa(offsetPrev)
+ links = append(links, fmt.Sprintf(
+ `<link rel="prev" href="%s" title="newer">`, href,
+ ))
+ refs.WriteString(makeA(href, "[prev]"))
+ }
+ if !errOccured {
+ href := urlPrefix + "/?offset=" + strconv.Itoa(offset+PageEntries)
+ links = append(links, fmt.Sprintf(
+ `<link rel="next" href="%s" title="older">`, href,
+ ))
+ refs.WriteString(makeA(href, "[next]"))
+ }
+ os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
+ out.Write([]byte(startHTML(
+ fmt.Sprintf("%s (%d-%d)", blogTitle, offset, offset+PageEntries),
+ links,
+ )))
+ out.Write(refs.Bytes())
+ out.Write(table.Bytes())
+ out.Write(refs.Bytes())
+ out.Write([]byte("\n"))
+ } else if pathInfo == "/"+AtomFeed {
+ commit, err = repo.CommitObject(head.Hash())
+ if err != nil {
+ makeErr(err)
+ }
+ etagHash.Write(commit.Hash[:])
+ etagHash.Write([]byte("ATOM"))
+ checkETag(etagHash)
+ feed := atom.Feed{
+ Title: blogTitle,
+ ID: atomId,
+ Updated: atom.Time(commit.Author.When),
+ Link: []atom.Link{{
+ Rel: "self",
+ Href: atomUrl,
+ }},
+ Author: &atom.Person{Name: atomAuthorName},
+ }
+ log, err := repo.Log(&git.LogOptions{From: head.Hash()})
+ if err != nil {
+ makeErr(err)
+ }
+ for i := 0; i < PageEntries; i++ {
+ commit, err = log.Next()
+ if err != nil {
+ break
+ }
+ lines := msgSplit(commit.Message)
+ feedId, err := uuid.FromBytes(commit.Hash[:16])
+ if err != nil {
+ panic(err)
+ }
+ feed.Entry = append(feed.Entry, &atom.Entry{
+ Title: lines[0],
+ ID: "urn:uuid:" + feedId.String(),
+ Link: []atom.Link{{
+ Rel: "alternate",
+ Href: blogBaseURL + urlPrefix + "/" + commit.Hash.String(),
+ }},
+ Published: atom.Time(commit.Author.When),
+ Updated: atom.Time(commit.Author.When),
+ Summary: &atom.Text{
+ Type: "text",
+ Body: lines[0],
+ },
+ Content: &atom.Text{
+ Type: "text",
+ Body: strings.Join(lines[2:], "\n"),
+ },
+ })
+ }
+ data, err := xml.MarshalIndent(&feed, "", " ")
+ if err != nil {
+ makeErr(err)
+ }
+ out.Write(data)
+ os.Stdout.WriteString("Content-Type: text/xml; charset=UTF-8\n")
+ os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
+ if gzipWriter != nil {
+ os.Stdout.WriteString("Content-Encoding: gzip\n")
+ gzipWriter.Close()
+ }
+ os.Stdout.WriteString("\n")
+ os.Stdout.Write(outBuf.Bytes())
+ return
+ } else if sha1DigestRe.MatchString(pathInfo[1:]) {
+ commit, err = repo.CommitObject(plumbing.NewHash(pathInfo[1:]))
+ if err != nil {
+ makeErr(err)
+ }
+ etagHash.Write(commit.Hash[:])
+ checkETag(etagHash)
+ lines := msgSplit(commit.Message)
+ title := lines[0]
+ when := commit.Author.When.Format(WhenFmt)
+ os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
+ parent := commit.ParentHashes[0].String()
+ out.Write([]byte(startHTML(fmt.Sprintf("%s (%s)", title, when), []string{
+ fmt.Sprintf(`<link rel="prev" href="%s" title="older">`, "/"+parent),
+ })))
+ out.Write([]byte(fmt.Sprintf(
+ "[%s] [<tt>%s</tt>]\n<hr/>\n",
+ makeA(urlPrefix+"/"+parent, "older"),
+ when,
+ )))
+ out.Write([]byte(fmt.Sprintf("<h2>%s</h2>\n<pre>\n", title)))
+ for _, line := range lines[2:] {
+ line = strings.ReplaceAll(line, "&", "&")
+ line = strings.ReplaceAll(line, "<", "<")
+ line = strings.ReplaceAll(line, ">", ">")
+ cols := strings.Split(line, " ")
+ for i, col := range cols {
+ if u := urlParse(col); u != nil {
+ cols[i] = makeA(col, col)
+ continue
+ }
+ cols[i] = sha1DigestRe.ReplaceAllString(col, makeA(urlPrefix+"/$1", "$1"))
+ }
+ line = strings.Join(cols, " ")
+ out.Write([]byte(line + "\n"))
+ }
+ out.Write([]byte("</pre>\n"))
+ if note := getNote(commit.Hash); note != "" {
+ out.Write([]byte(fmt.Sprintf("Note:\n<pre>\n%s</pre>\n", note)))
+ }
+ } else {
+ makeErr(errors.New("unknown URL action"))
+ }
+ if aboutUrl != "" {
+ out.Write([]byte(fmt.Sprintf(
+ "<hr/>%s %s\n",
+ makeA(aboutUrl, "About"),
+ blogTitle,
+ )))
+ }
+ out.Write([]byte("</body></html>\n"))
+ if gzipWriter != nil {
+ gzipWriter.Close()
+ }
+ os.Stdout.Write(outBuf.Bytes())
+}
--- /dev/null
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a h1:mEQZbbaBjWyLNy0tmZmgEuQAR8XOQ3hL8GYi3J/NG64=
+golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
+gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=