devtools/{depotview,hackdoc}: tie both together

Change-Id: I0a1ca3b4fa0e0a074eccbe0f8748839b926db9c1
diff --git a/devtools/hackdoc/source/source_depotview.go b/devtools/hackdoc/source/source_depotview.go
new file mode 100644
index 0000000..6a256be
--- /dev/null
+++ b/devtools/hackdoc/source/source_depotview.go
@@ -0,0 +1,126 @@
+package source
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	dvpb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
+)
+
+type DepotViewSourceProvider struct {
+	stub dvpb.DepotViewClient
+}
+
+func NewDepotView(stub dvpb.DepotViewClient) SourceProvider {
+	return &DepotViewSourceProvider{
+		stub: stub,
+	}
+}
+
+func changeRef(ref string) int64 {
+	ref = strings.ToLower(ref)
+	if !strings.HasPrefix(ref, "change/") && !strings.HasPrefix(ref, "cr/") {
+		return 0
+	}
+	n, err := strconv.ParseInt(strings.SplitN(ref, "/", 2)[1], 10, 64)
+	if err != nil {
+		return 0
+	}
+
+	return n
+}
+
+func (s *DepotViewSourceProvider) Source(ctx context.Context, ref string) (Source, error) {
+	var hash string
+	n := changeRef(ref)
+	if n != 0 {
+		res, err := s.stub.ResolveGerritChange(ctx, &dvpb.ResolveGerritChangeRequest{Change: n})
+		if err != nil {
+			return nil, err
+		}
+		hash = res.Hash
+	} else {
+		res, err := s.stub.Resolve(ctx, &dvpb.ResolveRequest{Ref: ref})
+		if err != nil {
+			return nil, err
+		}
+		hash = res.Hash
+	}
+
+	if hash == "" {
+		return nil, nil
+	}
+
+	return &depotViewSource{
+		stub:   s.stub,
+		hash:   hash,
+		change: n,
+	}, nil
+}
+
+type depotViewSource struct {
+	stub   dvpb.DepotViewClient
+	hash   string
+	change int64
+}
+
+func (s *depotViewSource) IsFile(ctx context.Context, path string) (bool, error) {
+	res, err := s.stub.Stat(ctx, &dvpb.StatRequest{
+		Hash: s.hash,
+		Path: path,
+	})
+	if err != nil {
+		return false, err
+	}
+	return res.Type == dvpb.StatResponse_TYPE_FILE, nil
+}
+
+func (s *depotViewSource) IsDirectory(ctx context.Context, path string) (bool, error) {
+	res, err := s.stub.Stat(ctx, &dvpb.StatRequest{
+		Hash: s.hash,
+		Path: path,
+	})
+	if err != nil {
+		return false, err
+	}
+	return res.Type == dvpb.StatResponse_TYPE_DIRECTORY, nil
+}
+
+func (s *depotViewSource) ReadFile(ctx context.Context, path string) ([]byte, error) {
+	var data []byte
+	srv, err := s.stub.Read(ctx, &dvpb.ReadRequest{
+		Hash: s.hash,
+		Path: path,
+	})
+	if err != nil {
+		return nil, err
+	}
+	for {
+		res, err := srv.Recv()
+		if err != nil {
+			return nil, err
+		}
+		if len(res.Data) == 0 {
+			break
+		}
+		data = append(data, res.Data...)
+	}
+	return data, nil
+}
+
+func (s *depotViewSource) WebLinks(fpath string) []WebLink {
+	gitURL := fmt.Sprintf(FlagGitwebURLPattern, s.hash, fpath)
+	links := []WebLink{
+		WebLink{Kind: "gitweb", LinkLabel: s.hash[:16], LinkURL: gitURL},
+	}
+
+	if s.change != 0 {
+		gerritLabel := fmt.Sprintf("change %d", s.change)
+		gerritLink := fmt.Sprintf("https://gerrit.hackerspace.pl/%d", s.change)
+		links = append(links, WebLink{Kind: "gerrit", LinkLabel: gerritLabel, LinkURL: gerritLink})
+	}
+
+	return links
+}