Add NewGitilesConfigFSRoot.

This exposes a git repository at multiple commit/tree IDs.

Change-Id: I10df27e74037d762e12ee3fbeaf2a929b89bb398
diff --git a/cmd/slothfs-gitilesfs/main.go b/cmd/slothfs-gitilesfs/main.go
index d08d362..273b890 100644
--- a/cmd/slothfs-gitilesfs/main.go
+++ b/cmd/slothfs-gitilesfs/main.go
@@ -28,7 +28,6 @@
 )
 
 func main() {
-	branch := flag.String("branch", "master", "Set the branch name.")
 	repo := flag.String("repo", "", "Set the repository name.")
 	debug := flag.Bool("debug", false, "Print FUSE debug info.")
 	cacheDir := flag.String("cache", filepath.Join(os.Getenv("HOME"), ".cache", "slothfs"),
@@ -40,7 +39,7 @@
 		log.Fatal("must set --cache")
 	}
 	if len(flag.Args()) < 1 {
-		log.Fatal("usage: main -gitiles URL -repo REPO [-branch BRANCH] MOUNT-POINT")
+		log.Fatal("usage: main -repo REPO MOUNT-POINT")
 	}
 
 	mntDir := flag.Arg(0)
@@ -60,17 +59,11 @@
 		log.Fatalf("GetProject(%s): %v", *repo, err)
 	}
 
-	tree, err := repoService.GetTree(*branch, "", true)
-	if err != nil {
-		log.Fatal(err)
-	}
-
 	opts := fs.GitilesOptions{
-		Revision: *branch,
 		CloneURL: project.CloneURL,
 	}
 
-	root := fs.NewGitilesRoot(cache, tree, repoService, opts)
+	root := fs.NewGitilesConfigFSRoot(cache, repoService, &opts)
 	server, _, err := nodefs.MountRoot(mntDir, root, &nodefs.Options{
 		EntryTimeout:    time.Hour,
 		NegativeTimeout: time.Hour,
diff --git a/fs/api.go b/fs/api.go
index c1ba8e8..5393755 100644
--- a/fs/api.go
+++ b/fs/api.go
@@ -26,10 +26,16 @@
 	Clone bool
 }
 
-// GitilesOptions configures the Gitiles filesystem.
-type GitilesOptions struct {
+// GitilesOptions configures the Gitiles filesystem (ie. Gitiles
+// backed FS) at a certain revision.
+type GitilesRevisionOptions struct {
 	Revision string
 
+	GitilesOptions
+}
+
+// GitilesOptions sets options for NewGitilesConfigRoot.
+type GitilesOptions struct {
 	// If set, clone the repo on reads from here.
 	CloneURL string
 
diff --git a/fs/gitilesconfigfs.go b/fs/gitilesconfigfs.go
new file mode 100644
index 0000000..73728e1
--- /dev/null
+++ b/fs/gitilesconfigfs.go
@@ -0,0 +1,87 @@
+// 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 (
+	"log"
+
+	"github.com/google/slothfs/cache"
+	"github.com/google/slothfs/gitiles"
+	"github.com/hanwen/go-fuse/fuse"
+	"github.com/hanwen/go-fuse/fuse/nodefs"
+	"github.com/libgit2/git2go"
+)
+
+type gitilesConfigFSRoot struct {
+	nodefs.Node
+
+	fsConn  *nodefs.FileSystemConnector
+	cache   *cache.Cache
+	service *gitiles.RepoService
+	options GitilesOptions
+}
+
+func (r *gitilesConfigFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
+	r.fsConn = fsConn
+}
+
+func (r *gitilesConfigFSRoot) Lookup(out *fuse.Attr, name string, context *fuse.Context) (*nodefs.Inode, fuse.Status) {
+	id, err := git.NewOid(name)
+	if err != nil {
+		return nil, fuse.ENOENT
+	}
+
+	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)
+			return nil, fuse.EIO
+		}
+
+		if err := r.cache.Tree.Add(id, tree); err != nil {
+			log.Println("TreeCache.Add(%s): %v", id, err)
+		}
+	}
+
+	gro := GitilesRevisionOptions{
+		Revision:       id.String(),
+		GitilesOptions: r.options,
+	}
+	newRoot := NewGitilesRoot(r.cache, tree, r.service, gro)
+	ch := r.Inode().NewChild(id.String(), true, newRoot)
+	out.Mode = fuse.S_IFDIR | 0755
+
+	newRoot.OnMount(r.fsConn)
+	return ch, fuse.OK
+}
+
+// NewGitilesConfigFSRoot returns a root node for a filesystem that lazily
+// instantiates a repository if you access any subdirectory named by a
+// 40-byte hex SHA1.
+func NewGitilesConfigFSRoot(c *cache.Cache, service *gitiles.RepoService, options *GitilesOptions) nodefs.Node {
+	// TODO(hanwen): nodefs.Node has an OnForget(), but it will
+	// never trigger for directories that have children. That
+	// means that we effectively never drop old trees. We can fix
+	// this by either: 1) reconsidering OnForget in go-fuse 2) do
+	// a periodic removal of all subtrees trees. Since the FS is
+	// read-only that should cause no ill effects.
+	return &gitilesConfigFSRoot{
+		Node:    nodefs.NewDefaultNode(),
+		cache:   c,
+		service: service,
+		options: *options,
+	}
+}
diff --git a/fs/gitilesfs.go b/fs/gitilesfs.go
index 8f4287d..40c22e3 100644
--- a/fs/gitilesfs.go
+++ b/fs/gitilesfs.go
@@ -43,7 +43,7 @@
 	cache   *cache.Cache
 	service *gitiles.RepoService
 	tree    *gitiles.Tree
-	opts    GitilesOptions
+	opts    GitilesRevisionOptions
 
 	handleLessIO bool
 
@@ -303,7 +303,7 @@
 }
 
 // NewGitilesRoot returns the root node for a file system.
-func NewGitilesRoot(c *cache.Cache, tree *gitiles.Tree, service *gitiles.RepoService, options GitilesOptions) nodefs.Node {
+func NewGitilesRoot(c *cache.Cache, tree *gitiles.Tree, service *gitiles.RepoService, options GitilesRevisionOptions) nodefs.Node {
 	r := &gitilesRoot{
 		Node:         newDirNode(),
 		service:      service,
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index 88a826c..3c033e3 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -15,6 +15,7 @@
 package fs
 
 import (
+	"bytes"
 	"encoding/base64"
 	"fmt"
 	"io/ioutil"
@@ -38,11 +39,15 @@
 
 const fuseDebug = false
 
+const testEncodedBlob = `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`
+
+var testBlob []byte
+
 func init() {
 	enc := map[string]string{
-		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS?format=TEXT":  `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
-		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORSx?format=TEXT": `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
-		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS2?format=TEXT": `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
+		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS?format=TEXT":  testEncodedBlob,
+		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORSx?format=TEXT": testEncodedBlob,
+		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS2?format=TEXT": testEncodedBlob,
 		"/platform/build/kati/+/ce34badf691d36e8048b63f89d1a86ee5fa4325c/testcase/addprefix.mk":    "dGVzdDoKCWVjaG8gJChhZGRwcmVmaXggc3JjLyxmb28gYmFyKQo=",
 	}
 	for k, v := range enc {
@@ -53,6 +58,10 @@
 		}
 
 		c = c[:n]
+		if v == testEncodedBlob {
+			testBlob = c
+		}
+
 		testGitiles[k] = string(c)
 	}
 }
@@ -241,9 +250,11 @@
 		t.Fatal("Tree:", err)
 	}
 
-	options := GitilesOptions{
+	options := GitilesRevisionOptions{
 		Revision: "ce34badf691d36e8048b63f89d1a86ee5fa4325c",
-		CloneURL: fmt.Sprintf("http://%s/platform/build/kati", fix.testServer.addr),
+		GitilesOptions: GitilesOptions{
+			CloneURL: fmt.Sprintf("http://%s/platform/build/kati", fix.testServer.addr),
+		},
 	}
 
 	fs := NewGitilesRoot(fix.cache, treeResp, repoService, options)
@@ -269,7 +280,7 @@
 		t.Fatal("Tree:", err)
 	}
 
-	options := GitilesOptions{}
+	options := GitilesRevisionOptions{}
 
 	fs := NewGitilesRoot(fix.cache, treeResp, repoService, options)
 	if err := fix.mount(fs); err != nil {
@@ -308,7 +319,7 @@
 		t.Fatal("Tree:", err)
 	}
 
-	options := GitilesOptions{}
+	options := GitilesRevisionOptions{}
 
 	fs := NewGitilesRoot(fix.cache, treeResp, repoService, options)
 	if err := fix.mount(fs); err != nil {
@@ -358,7 +369,7 @@
 			ID:   "ce34badf691d36e8048b63f89d1a86ee5fa4325c",
 		}},
 	}
-	fs := NewGitilesRoot(fix.cache, tree, repoService, GitilesOptions{})
+	fs := NewGitilesRoot(fix.cache, tree, repoService, GitilesRevisionOptions{})
 	if err := fix.mount(fs); err != nil {
 		t.Fatal("mount", err)
 	}
@@ -392,9 +403,8 @@
 		t.Fatal("Tree:", err)
 	}
 
-	options := GitilesOptions{
-		CloneOption: fileOpts,
-	}
+	options := GitilesRevisionOptions{}
+	options.CloneOption = fileOpts
 
 	fs := NewGitilesRoot(fix.cache, treeResp, repoService, options)
 	if err := fix.mount(fs); err != nil {
@@ -443,7 +453,7 @@
 		t.Fatal("Tree:", err)
 	}
 
-	options := GitilesOptions{
+	options := GitilesRevisionOptions{
 		Revision: "ce34badf691d36e8048b63f89d1a86ee5fa4325c",
 	}
 
@@ -486,7 +496,7 @@
 		t.Fatal("Tree:", err)
 	}
 
-	fs := NewGitilesRoot(fix.cache, treeResp, repoService, GitilesOptions{})
+	fs := NewGitilesRoot(fix.cache, treeResp, repoService, GitilesRevisionOptions{})
 	if err := fix.mount(fs); err != nil {
 		t.Fatal("mount", err)
 	}
@@ -521,7 +531,7 @@
 		t.Fatal("Tree:", err)
 	}
 
-	options := GitilesOptions{
+	options := GitilesRevisionOptions{
 		Revision: "ce34badf691d36e8048b63f89d1a86ee5fa4325c",
 	}
 
@@ -548,3 +558,30 @@
 		}
 	}
 }
+
+func TestGitilesConfigFSTest(t *testing.T) {
+	fix, err := newTestFixture()
+	if err != nil {
+		t.Fatal("newTestFixture", err)
+	}
+	defer fix.cleanup()
+
+	repoService := fix.service.NewRepoService("platform/build/kati")
+	if err != nil {
+		t.Fatal("Tree:", err)
+	}
+
+	fs := NewGitilesConfigFSRoot(fix.cache, repoService, &GitilesOptions{})
+	if err := fix.mount(fs); err != nil {
+		t.Fatal("mount", err)
+	}
+
+	fn := filepath.Join(fix.mntDir, "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/manifestfs.go b/fs/manifestfs.go
index 3f60986..1c96928 100644
--- a/fs/manifestfs.go
+++ b/fs/manifestfs.go
@@ -143,10 +143,12 @@
 
 			repoService := r.service.NewRepoService(revmap[p].Name)
 
-			opts := GitilesOptions{
-				Revision:    revmap[p].Revision,
-				CloneURL:    cloneURL,
-				CloneOption: r.options.FileCloneOption,
+			opts := GitilesRevisionOptions{
+				Revision: revmap[p].Revision,
+				GitilesOptions: GitilesOptions{
+					CloneURL:    cloneURL,
+					CloneOption: r.options.FileCloneOption,
+				},
 			}
 
 			subRoot := NewGitilesRoot(r.cache, r.trees[p], repoService, opts)