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 } 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 } 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, "/") tree, errStatusCode, err := h.getTree(r) if err != nil { http.Error(rw, err.Error(), errStatusCode) 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) }