Add gitiles host FS.

This creates a tree representing all repos on a gitiles host.

Change-Id: Ida8f7b88fc4c4f4b8503ea696838c6b44e125fca
diff --git a/cmd/slothfs-hostfs/main.go b/cmd/slothfs-hostfs/main.go
new file mode 100644
index 0000000..68a80b6
--- /dev/null
+++ b/cmd/slothfs-hostfs/main.go
@@ -0,0 +1,71 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/google/slothfs/cache"
+	"github.com/google/slothfs/fs"
+	"github.com/google/slothfs/gitiles"
+	"github.com/hanwen/go-fuse/fuse/nodefs"
+)
+
+func main() {
+	debug := flag.Bool("debug", false, "Print FUSE debug info.")
+	cacheDir := flag.String("cache", filepath.Join(os.Getenv("HOME"), ".cache", "slothfs"),
+		"Set directory for file system cache.")
+	gitilesOptions := gitiles.DefineFlags()
+	flag.Parse()
+
+	if *cacheDir == "" {
+		log.Fatal("must set --cache")
+	}
+	if len(flag.Args()) < 1 {
+		log.Fatal("usage: main MOUNT-POINT")
+	}
+
+	mntDir := flag.Arg(0)
+	cache, err := cache.NewCache(*cacheDir, cache.Options{})
+	if err != nil {
+		log.Fatalf("NewCache: %v", err)
+	}
+
+	service, err := gitiles.NewService(*gitilesOptions)
+	if err != nil {
+		log.Fatalf("NewService: %v", err)
+	}
+
+	root, err := fs.NewHostFS(cache, service, nil)
+	if err != nil {
+		log.Fatalf("NewService: %v", err)
+	}
+
+	server, _, err := nodefs.MountRoot(mntDir, root, &nodefs.Options{
+		EntryTimeout:    time.Hour,
+		NegativeTimeout: time.Hour,
+		AttrTimeout:     time.Hour,
+		Debug:           *debug,
+	})
+	if err != nil {
+		log.Fatalf("MountFileSystem: %v", err)
+	}
+	log.Printf("Started gitiles fs FUSE on %s", mntDir)
+	server.Serve()
+}
diff --git a/fs/gitilesconfigfs.go b/fs/gitilesconfigfs.go
index 73728e1..ce4fda1 100644
--- a/fs/gitilesconfigfs.go
+++ b/fs/gitilesconfigfs.go
@@ -43,16 +43,20 @@
 		return nil, fuse.ENOENT
 	}
 
+	if ch := r.Inode().GetChild(name); ch != nil {
+		return ch, fuse.OK
+	}
+
 	tree, err := r.cache.Tree.Get(id)
 	if err != nil {
 		tree, err = r.service.GetTree(id.String(), "/", true)
 		if err != nil {
-			log.Println("GetTree(%s): %v", id, err)
+			log.Printf("GetTree(%s): %v", id, err)
 			return nil, fuse.EIO
 		}
 
 		if err := r.cache.Tree.Add(id, tree); err != nil {
-			log.Println("TreeCache.Add(%s): %v", id, err)
+			log.Printf("TreeCache.Add(%s): %v", id, err)
 		}
 	}
 
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index 3c033e3..f444292 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -93,6 +93,15 @@
 
 var testGitiles = map[string]string{
 	"/platform/manifest/+show/master/default.xml?format=TEXT": testManifestXML,
+	"/?format=JSON": `)]}'
+{
+  "platform/build/kati": {
+    "name": "platform/build/kati",
+    "clone_url": "https://android.googlesource.com/platform/build/kati",
+    "description": "Description."
+  }
+}
+`,
 	"/platform/build/kati/+/master?format=JSON": `)]}'
 {
   "commit": "ce34badf691d36e8048b63f89d1a86ee5fa4325c",
@@ -585,3 +594,26 @@
 		t.Errorf("blob for %s differs", fn)
 	}
 }
+
+func TestGitilesHostFS(t *testing.T) {
+	fix, err := newTestFixture()
+	if err != nil {
+		t.Fatal("newTestFixture", err)
+	}
+	defer fix.cleanup()
+
+	if fs, err := NewHostFS(fix.cache, fix.service, nil); err != nil {
+		t.Fatalf("NewHostFS: %v", err)
+	} else if err := fix.mount(fs); err != nil {
+		t.Fatalf("mount: %v", err)
+	}
+
+	fn := filepath.Join(fix.mntDir, "platform/build/kati", "ce34badf691d36e8048b63f89d1a86ee5fa4325c", "AUTHORS")
+	content, err := ioutil.ReadFile(fn)
+	if err != nil {
+		t.Fatalf("ReadFile: %v", err)
+	}
+	if bytes.Compare(content, testBlob) != 0 {
+		t.Errorf("blob for %s differs", fn)
+	}
+}
diff --git a/fs/gitileshostfs.go b/fs/gitileshostfs.go
new file mode 100644
index 0000000..7c83e6d
--- /dev/null
+++ b/fs/gitileshostfs.go
@@ -0,0 +1,121 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fs
+
+import (
+	"fmt"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/google/slothfs/cache"
+	"github.com/google/slothfs/gitiles"
+	"github.com/hanwen/go-fuse/fuse/nodefs"
+)
+
+type hostFS struct {
+	nodefs.Node
+
+	cache        *cache.Cache
+	service      *gitiles.Service
+	projects     map[string]*gitiles.Project
+	cloneOptions []CloneOption
+}
+
+func parents(projMap map[string]*gitiles.Project) map[string]struct{} {
+	dirs := map[string]struct{}{}
+	for nm := range projMap {
+		for nm != "" && nm != "." {
+			next := filepath.Dir(nm)
+			dirs[next] = struct{}{}
+			nm = next
+		}
+	}
+	return dirs
+}
+
+func NewHostFS(cache *cache.Cache, service *gitiles.Service, cloneOptions []CloneOption) (*hostFS, error) {
+	projMap, err := service.List(nil)
+	if err != nil {
+		return nil, err
+	}
+
+	dirs := parents(projMap)
+	for p := range projMap {
+		if _, ok := dirs[p]; ok {
+			return nil, fmt.Errorf("%q is a dir and a project", p)
+		}
+	}
+
+	return &hostFS{
+		Node:         nodefs.NewDefaultNode(),
+		projects:     projMap,
+		cloneOptions: cloneOptions,
+		service:      service,
+		cache:        cache,
+	}, nil
+}
+
+func (h *hostFS) OnMount(fsConn *nodefs.FileSystemConnector) {
+	var keys []string
+	for k := range parents(h.projects) {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	nodes := map[string]*nodefs.Inode{
+		"": h.Inode(),
+	}
+
+	for _, k := range keys {
+		if k == "." {
+			continue
+		}
+		d, nm := filepath.Split(k)
+		d = strings.TrimSuffix(d, "/")
+		parent := nodes[d]
+
+		var node nodefs.Node
+		if p := h.projects[k]; p != nil {
+			node = h.newProjectNode(parent, p)
+			delete(h.projects, k)
+			node.OnMount(fsConn)
+		} else {
+			node = newDirNode()
+		}
+		ch := parent.NewChild(nm, true, node)
+		nodes[k] = ch
+	}
+
+	for k, p := range h.projects {
+		d, nm := filepath.Split(k)
+		d = strings.TrimSuffix(d, "/")
+
+		parent := nodes[d]
+		node := h.newProjectNode(parent, p)
+		node.OnMount(fsConn)
+
+		parent.NewChild(nm, true, node)
+	}
+}
+
+func (h *hostFS) newProjectNode(parent *nodefs.Inode, proj *gitiles.Project) nodefs.Node {
+	repoService := h.service.NewRepoService(proj.Name)
+	opts := GitilesOptions{
+		CloneURL:    proj.CloneURL,
+		CloneOption: h.cloneOptions,
+	}
+	return NewGitilesConfigFSRoot(h.cache, repoService, &opts)
+}
diff --git a/gitiles/client.go b/gitiles/client.go
index 8bc070c..90c4f54 100644
--- a/gitiles/client.go
+++ b/gitiles/client.go
@@ -194,6 +194,12 @@
 
 	projects := map[string]*Project{}
 	err := s.getJSON(&listURL, &projects)
+	for k, v := range projects {
+		if k != v.Name {
+			return nil, fmt.Errorf("gitiles: key %q had project name %q", k, v.Name)
+		}
+	}
+
 	return projects, err
 }