devtools/hackdoc: init

This is hackdoc, a documentation rendering tool for monorepos.

This is the first code iteration, that can only serve from a local git
checkout.

The code is incomplete, and is WIP.

Change-Id: I68ef7a991191c1bb1b0fdd2a8d8353aba642e28f
diff --git a/devtools/hackdoc/source/source_local.go b/devtools/hackdoc/source/source_local.go
new file mode 100644
index 0000000..35ad172
--- /dev/null
+++ b/devtools/hackdoc/source/source_local.go
@@ -0,0 +1,78 @@
+package source
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+type LocalSource struct {
+	root string
+}
+
+func NewLocal(root string) Source {
+	return &LocalSource{
+		root: strings.TrimRight(root, "/"),
+	}
+}
+
+func (s *LocalSource) resolve(path string) (string, error) {
+	if !strings.HasPrefix(path, "//") {
+		return "", fmt.Errorf("invalid path %q, expected // prefix", path)
+	}
+	path = path[2:]
+	if strings.HasPrefix(path, "/") {
+		return "", fmt.Errorf("invalid path %q, expected // prefix", path)
+	}
+
+	return s.root + "/" + path, nil
+}
+
+func (s *LocalSource) IsFile(path string) (bool, error) {
+	path, err := s.resolve(path)
+	if err != nil {
+		return false, err
+	}
+	stat, err := os.Stat(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("os.Stat(%q): %w", path, err)
+	}
+	return !stat.IsDir(), nil
+}
+
+func (s *LocalSource) ReadFile(path string) ([]byte, error) {
+	path, err := s.resolve(path)
+	if err != nil {
+		return nil, err
+	}
+	// TODO(q3k): limit size
+	return ioutil.ReadFile(path)
+}
+
+func (s *LocalSource) IsDirectory(path string) (bool, error) {
+	path, err := s.resolve(path)
+	if err != nil {
+		return false, err
+	}
+	stat, err := os.Stat(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, nil
+		}
+		return false, fmt.Errorf("os.Stat(%q): %w", path, err)
+	}
+	return stat.IsDir(), nil
+}
+
+func (s *LocalSource) CacheSet(dependencies []string, key string, value interface{}) {
+	// Swallow writes. The local filesystem can always change underneath us and
+	// we're not tracking anything, so we cannot ever keep caches.
+}
+
+func (s *LocalSource) CacheGet(key string) interface{} {
+	return nil
+}