2020-03-27 02:25:54 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"text/template"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"gitlab.com/golang-commonmark/markdown"
|
|
|
|
"gopkg.in/src-d/go-git.v4"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
|
|
)
|
|
|
|
|
|
|
|
type handler struct {
|
|
|
|
repo *git.Repository
|
|
|
|
branch plumbing.ReferenceName
|
|
|
|
tpl *template.Template
|
|
|
|
}
|
|
|
|
|
2020-03-29 16:56:04 +00:00
|
|
|
func (h handler) getTree(r *http.Request) (*object.Tree, int, error) {
|
|
|
|
rev := plumbing.Revision(r.FormValue("rev"))
|
|
|
|
if rev == "" {
|
|
|
|
rev = plumbing.Revision(h.branch)
|
|
|
|
}
|
|
|
|
|
|
|
|
hashPtr, err := h.repo.ResolveRevision(rev)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 404, fmt.Errorf("resolving revision %q: %w", rev, err)
|
|
|
|
}
|
|
|
|
hash := *hashPtr // I don't know why ResolveRevision returns a pointer
|
|
|
|
|
|
|
|
commit, err := h.repo.CommitObject(hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 404, fmt.Errorf("retrieving commit for revision %q (%q): %w",
|
|
|
|
rev, hash, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
tree, err := h.repo.TreeObject(commit.TreeHash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 500, fmt.Errorf("fetching tree %q of commit %q: %v",
|
|
|
|
commit.TreeHash, hash, err)
|
|
|
|
}
|
|
|
|
return tree, 0, nil
|
|
|
|
}
|
|
|
|
|
2020-03-27 02:25:54 +00:00
|
|
|
func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
path := r.URL.Path
|
|
|
|
var mdPath string
|
|
|
|
if strings.HasSuffix(path, "/") {
|
|
|
|
mdPath = filepath.Join(path, "README.md") // do before modifying path
|
|
|
|
path = filepath.Join(path, "index.html")
|
|
|
|
|
|
|
|
} else if strings.HasSuffix(path, "/index.html") {
|
|
|
|
mdPath = filepath.Join(filepath.Dir(path), "README.md")
|
|
|
|
|
|
|
|
} else if filepath.Ext(path) == ".html" {
|
|
|
|
mdPath = strings.TrimSuffix(path, ".html") + ".md"
|
|
|
|
}
|
|
|
|
|
|
|
|
path = strings.TrimPrefix(path, "/")
|
|
|
|
mdPath = strings.TrimPrefix(mdPath, "/")
|
|
|
|
|
2020-03-29 16:56:04 +00:00
|
|
|
tree, errStatusCode, err := h.getTree(r)
|
2020-03-27 02:25:54 +00:00
|
|
|
if err != nil {
|
2020-03-29 16:56:04 +00:00
|
|
|
http.Error(rw, err.Error(), errStatusCode)
|
2020-03-27 02:25:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var usingMD bool
|
|
|
|
f, err := tree.File(path)
|
|
|
|
if errors.Is(err, object.ErrFileNotFound) {
|
|
|
|
usingMD = true
|
|
|
|
f, err = tree.File(mdPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
if errors.Is(err, object.ErrFileNotFound) {
|
|
|
|
http.Error(rw, fmt.Sprintf("%q not found", path), 404)
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
|
|
|
log.Printf("fetching file %q / %q: %v", path, mdPath, err)
|
|
|
|
http.Error(rw, "internal error", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fr, err := f.Blob.Reader()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("getting reader of file %q: %v", f.Name, err)
|
|
|
|
http.Error(rw, "internal error", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer fr.Close()
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(fr)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("reading in contents of file %q: %v", f.Name, err)
|
|
|
|
http.Error(rw, "internal error", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !usingMD {
|
|
|
|
http.ServeContent(rw, r, filepath.Base(path), time.Now(), bytes.NewReader(b))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
mdBuf := new(bytes.Buffer)
|
|
|
|
if err := markdown.New().Render(mdBuf, b); err != nil {
|
|
|
|
log.Printf("rendering file %q to markdown: %v", f.Name, err)
|
|
|
|
http.Error(rw, "internal error", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if h.tpl == nil {
|
|
|
|
http.ServeContent(rw, r, filepath.Base(path), time.Now(), bytes.NewReader(mdBuf.Bytes()))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
h.tpl.Execute(rw, struct {
|
|
|
|
Body string
|
|
|
|
}{mdBuf.String()})
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
addr := flag.String("addr", ":8000", "Address to listen for http requests on")
|
|
|
|
branchName := flag.String("branch", "master", "git branch to serve the HEAD of")
|
|
|
|
repoPath := flag.String("repo-path", ".", "Path to the git repository to server")
|
|
|
|
tplPath := flag.String("tpl-path", "", "Path to an optional template file which can be used when rendering markdown")
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
repo, err := git.PlainOpen(*repoPath)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("opening git repo at path %q: %v", *repoPath, err)
|
|
|
|
}
|
|
|
|
branch := plumbing.NewBranchReferenceName(*branchName)
|
|
|
|
|
|
|
|
// do an initial check for the branch, for funsies
|
|
|
|
if _, err := repo.Reference(branch, true); err != nil {
|
|
|
|
log.Fatalf("resolving reference %q: %v", branch, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
h := &handler{
|
|
|
|
repo: repo,
|
|
|
|
branch: branch,
|
|
|
|
}
|
|
|
|
|
|
|
|
if *tplPath != "" {
|
|
|
|
h.tpl = template.Must(template.ParseFiles(*tplPath))
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("listening on %q", *addr)
|
|
|
|
http.ListenAndServe(*addr, h)
|
|
|
|
}
|