devtools/{depotview,hackdoc}: tie both together

Change-Id: I0a1ca3b4fa0e0a074eccbe0f8748839b926db9c1
diff --git a/devtools/hackdoc/main.go b/devtools/hackdoc/main.go
index 938a426..558268b 100644
--- a/devtools/hackdoc/main.go
+++ b/devtools/hackdoc/main.go
@@ -1,24 +1,30 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"net/http"
 	"path/filepath"
 	"regexp"
 	"strings"
+	"time"
 
+	"code.hackerspace.pl/hscloud/go/mirko"
+	"code.hackerspace.pl/hscloud/go/pki"
 	"github.com/golang/glog"
+	"google.golang.org/grpc"
 
+	dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
 	"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
 	"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
 )
 
 var (
 	flagListen              = "127.0.0.1:8080"
-	flagDocRoot             = "./docroot"
+	flagDocRoot             = ""
+	flagDepotViewAddress    = ""
 	flagHackdocURL          = ""
-	flagGitwebURLPattern    = "https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/%s/%s"
 	flagGitwebDefaultBranch = "master"
 
 	rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`)
@@ -29,10 +35,11 @@
 }
 
 func main() {
-	flag.StringVar(&flagListen, "listen", flagListen, "Address to listen on for HTTP traffic")
-	flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents")
+	flag.StringVar(&flagListen, "pub_listen", flagListen, "Address to listen on for HTTP traffic")
+	flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents. Either this or depotview must be set")
+	flag.StringVar(&flagDepotViewAddress, "depotview", flagDepotViewAddress, "gRPC endpoint of depotview to serve from Git. Either this or docroot must be set")
 	flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes")
-	flag.StringVar(&flagGitwebURLPattern, "gitweb_url_pattern", flagGitwebURLPattern, "Pattern to sprintf to for URL for viewing a file in Git. First string is ref/rev, second is bare file path (sans //)")
+	flag.StringVar(&source.FlagGitwebURLPattern, "gitweb_url_pattern", source.FlagGitwebURLPattern, "Pattern to sprintf to for URL for viewing a file in Git. First string is ref/rev, second is bare file path (sans //)")
 	flag.StringVar(&flagGitwebDefaultBranch, "gitweb_default_rev", flagGitwebDefaultBranch, "Default Git rev to render/link to")
 	flag.Parse()
 
@@ -40,25 +47,61 @@
 		flagHackdocURL = fmt.Sprintf("http://%s", flagListen)
 	}
 
-	path, err := filepath.Abs(flagDocRoot)
-	if err != nil {
-		glog.Fatalf("Could not dereference path %q: %w", path, err)
+	if flagDocRoot == "" && flagDepotViewAddress == "" {
+		glog.Errorf("Either -docroot or -depotview must be set")
+	}
+	if flagDocRoot != "" && flagDepotViewAddress != "" {
+		glog.Errorf("Only one of -docroot or -depotview must be set")
 	}
 
-	s := &service{
-		source: source.NewLocal(path),
+	m := mirko.New()
+	if err := m.Listen(); err != nil {
+		glog.Exitf("Listen(): %v", err)
 	}
 
-	http.HandleFunc("/", s.handler)
+	var s *service
+	if flagDocRoot != "" {
+		path, err := filepath.Abs(flagDocRoot)
+		if err != nil {
+			glog.Exitf("Could not dereference path %q: %w", path, err)
+		}
+		glog.Infof("Starting in docroot mode for %q -> %q", flagDocRoot, path)
+
+		s = &service{
+			source: source.NewSingleRefProvider(source.NewLocal(path)),
+		}
+	} else {
+		glog.Infof("Starting in depotview mode (server %q)", flagDepotViewAddress)
+		conn, err := grpc.Dial(flagDepotViewAddress, pki.WithClientHSPKI())
+		if err != nil {
+			glog.Exitf("grpc.Dial(%q): %v", flagDepotViewAddress, err)
+		}
+		stub := dvpb.NewDepotViewClient(conn)
+		s = &service{
+			source: source.NewDepotView(stub),
+		}
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", s.handler)
+	srv := &http.Server{Addr: flagListen, Handler: mux}
 
 	glog.Infof("Listening on %q...", flagListen)
-	if err := http.ListenAndServe(flagListen, nil); err != nil {
-		glog.Fatal(err)
-	}
+	go func() {
+		if err := srv.ListenAndServe(); err != nil {
+			glog.Error(err)
+		}
+	}()
+
+	<-m.Done()
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	srv.Shutdown(ctx)
+
 }
 
 type service struct {
-	source source.Source
+	source source.SourceProvider
 }
 
 func (s *service) handler(w http.ResponseWriter, r *http.Request) {
@@ -69,23 +112,60 @@
 	}
 
 	glog.Infof("%+v", r.URL.Query())
-	rev := r.URL.Query().Get("rev")
-	if rev == "" {
-		rev = flagGitwebDefaultBranch
+	ref := r.URL.Query().Get("ref")
+	if ref == "" {
+		ref = flagGitwebDefaultBranch
+	}
+
+	ctx := r.Context()
+	source, err := s.source.Source(ctx, ref)
+	switch {
+	case err != nil:
+		glog.Errorf("Source(%q): %v", ref, err)
+		handle500(w, r)
+		return
+	case source == nil:
+		handle404(w, r)
+		return
 	}
 
 	path := r.URL.Path
 
 	if match := rePagePath.FindStringSubmatch(path); match != nil {
-		s.handlePage(w, r, rev, match[1])
+		req := &request{
+			w:      w,
+			r:      r,
+			ctx:    r.Context(),
+			ref:    ref,
+			source: source,
+		}
+		req.handlePage(match[1])
 		return
 	}
 	handle404(w, r)
 }
 
-func logRequest(w http.ResponseWriter, r *http.Request, format string, args ...interface{}) {
-	result := fmt.Sprintf(format, args...)
-	glog.Infof("result: %s, remote: %q, ua: %q, referrer: %q, host: %q path: %q", result, r.RemoteAddr, r.Header.Get("User-Agent"), r.Header.Get("Referrer"), r.Host, r.URL.Path)
+type request struct {
+	w   http.ResponseWriter
+	r   *http.Request
+	ctx context.Context
+
+	ref    string
+	source source.Source
+	// rpath is the path requested by the client
+	rpath string
+}
+
+func (r *request) handle500() {
+	handle500(r.w, r.r)
+}
+
+func (r *request) handle404() {
+	handle404(r.w, r.r)
+}
+
+func (r *request) logRequest(format string, args ...interface{}) {
+	logRequest(r.w, r.r, format, args...)
 }
 
 func urlPathToDepotPath(url string) string {
@@ -109,70 +189,69 @@
 	return path
 }
 
-func (s *service) handlePageAuto(w http.ResponseWriter, r *http.Request, rev, rpath, dirpath string) {
-	cfg, err := config.ForPath(s.source, dirpath)
+func (r *request) handlePageAuto(dirpath string) {
+	cfg, err := config.ForPath(r.ctx, r.source, dirpath)
 	if err != nil {
 		glog.Errorf("could not get config for path %q: %w", dirpath, err)
-		handle500(w, r)
+		r.handle500()
 		return
 	}
 	for _, f := range cfg.DefaultIndex {
 		fpath := dirpath + f
-		file, err := s.source.IsFile(fpath)
+		file, err := r.source.IsFile(r.ctx, fpath)
 		if err != nil {
 			glog.Errorf("IsFile(%q): %w", fpath, err)
-			handle500(w, r)
+			r.handle500()
 			return
 		}
 
 		if file {
-			s.handleMarkdown(w, r, s.source, rev, fpath, cfg)
+			r.handleMarkdown(fpath, cfg)
 			return
 		}
 	}
-
-	handle404(w, r)
+	r.handle404()
 }
 
-func (s *service) handlePage(w http.ResponseWriter, r *http.Request, rev, page string) {
-	path := urlPathToDepotPath(page)
+func (r *request) handlePage(page string) {
+	r.rpath = urlPathToDepotPath(page)
 
-	if strings.HasSuffix(path, "/") {
+	if strings.HasSuffix(r.rpath, "/") {
 		// Directory path given, autoresolve.
-		dirpath := path
-		if path != "//" {
-			dirpath = strings.TrimSuffix(path, "/") + "/"
+		dirpath := r.rpath
+		if r.rpath != "//" {
+			dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
 		}
-		s.handlePageAuto(w, r, rev, path, dirpath)
+		r.handlePageAuto(dirpath)
 		return
 	}
 
 	// Otherwise, try loading the file.
-	file, err := s.source.IsFile(path)
+	file, err := r.source.IsFile(r.ctx, r.rpath)
 	if err != nil {
-		glog.Errorf("IsFile(%q): %w", path, err)
-		handle500(w, r)
+		glog.Errorf("IsFile(%q): %w", r.rpath, err)
+		r.handle500()
 		return
 	}
 
 	// File exists, render that.
 	if file {
-		parts := strings.Split(path, "/")
+		parts := strings.Split(r.rpath, "/")
 		dirpath := strings.Join(parts[:(len(parts)-1)], "/")
-		cfg, err := config.ForPath(s.source, dirpath)
+		cfg, err := config.ForPath(r.ctx, r.source, dirpath)
 		if err != nil {
 			glog.Errorf("could not get config for path %q: %w", dirpath, err)
-			handle500(w, r)
+			r.handle500()
 			return
 		}
-		s.handleMarkdown(w, r, s.source, rev, path, cfg)
+		r.handleMarkdown(r.rpath, cfg)
 		return
 	}
 
 	// Otherwise assume directory, try all posibilities.
-	dirpath := path
-	if path != "//" {
-		dirpath = strings.TrimSuffix(path, "/") + "/"
+	dirpath := r.rpath
+	if r.rpath != "//" {
+		dirpath = strings.TrimSuffix(r.rpath, "/") + "/"
 	}
-	s.handlePageAuto(w, r, rev, path, dirpath)
+	r.handlePageAuto(dirpath)
 }