Handle symlinks in populate:
* Accept but ignore Utimens() on directory nodes. This is for symlinks
to include/ directories
* Return added and changed files separately. This is so we don't touch
everything for a sloth-populate call into an empty workspace.
* Cleanup e2e test code, and add a case for a changing symlink.
Change-Id: I743b9887182077bf9c63d660031b92823fbeb1f8
diff --git a/cmd/slothfs-populate/main.go b/cmd/slothfs-populate/main.go
index 5c50d79..c130edf 100644
--- a/cmd/slothfs-populate/main.go
+++ b/cmd/slothfs-populate/main.go
@@ -34,16 +34,24 @@
log.Fatal("too many arguments.")
}
- changed, err := populate.Checkout(*mount, dir)
+ added, changed, err := populate.Checkout(*mount, dir)
if err != nil {
log.Fatalf("populate.Checkout: %v", err)
}
- now := time.Now()
- for _, c := range changed {
- if err := os.Chtimes(c, now, now); err != nil {
- log.Fatalf("Chtimes(%s): %v", c, err)
+ if len(changed) > 0 {
+ now := time.Now()
+ n := 0
+ for _, slice := range [][]string{added, changed} {
+ for _, c := range slice {
+ if err := os.Chtimes(c, now, now); err != nil {
+ log.Fatalf("Chtimes(%s): %v", c, err)
+ }
+ n++
+ }
}
+ log.Printf("touched %d files", n)
+ } else {
+ log.Printf("no files were changed, %d were added; assuming fresh checkout.", len(added))
}
- log.Printf("touched %d files", len(changed))
}
diff --git a/fs/gitilesfs.go b/fs/gitilesfs.go
index de4661c..a43c5fd 100644
--- a/fs/gitilesfs.go
+++ b/fs/gitilesfs.go
@@ -330,6 +330,12 @@
nodefs.Node
}
+// Implement Utimens so we don't create spurious "not implemented"
+// messages when directory targets for symlinks are touched.
+func (n *dirNode) Utimens(file nodefs.File, atime *time.Time, mtime *time.Time, context *fuse.Context) (code fuse.Status) {
+ return fuse.OK
+}
+
func (n *dirNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) {
out.Mode = fuse.S_IFDIR | 0755
t := time.Unix(1, 0)
diff --git a/populate/e2e_test.go b/populate/e2e_test.go
index 47223ff..9f141cf 100644
--- a/populate/e2e_test.go
+++ b/populate/e2e_test.go
@@ -7,12 +7,14 @@
"net"
"os"
"path/filepath"
+ "reflect"
"testing"
"github.com/google/slothfs/cache"
"github.com/google/slothfs/fs"
"github.com/google/slothfs/gitiles"
"github.com/google/slothfs/manifest"
+ "github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/fuse/nodefs"
git "github.com/libgit2/git2go"
@@ -38,58 +40,110 @@
return &i
}
+func newString(s string) *string {
+ return &s
+}
+
func abortListener(l net.Listener) {
- _, err := l.Accept()
- if err == nil {
- log.Panicf("got incoming connection")
+ for {
+ conn, err := l.Accept()
+ if err != nil {
+ break
+ }
+ conn.Close()
}
}
-func TestFUSE(t *testing.T) {
+type fixture struct {
+ dir string
+ cache *cache.Cache
+ fsServer *fuse.Server
+ abortGitiles net.Listener
+}
+
+func (f *fixture) Cleanup() {
+ if f.abortGitiles != nil {
+ f.abortGitiles.Close()
+ }
+ if f.fsServer != nil {
+ if err := f.fsServer.Unmount(); err != nil {
+ return
+ }
+ }
+ os.RemoveAll(f.dir)
+}
+
+func (f *fixture) addWorkspace(name string, mf *manifest.Manifest) error {
+ bytes1, err := mf.MarshalXML()
+ if err != nil {
+ return err
+ }
+
+ dir := f.dir
+
+ if err := ioutil.WriteFile(filepath.Join(dir, name+".xml"), bytes1, 0644); err != nil {
+ return err
+ }
+ if err := os.Symlink(filepath.Join(dir, name+".xml"), filepath.Join(dir, "mnt", "config", name)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func newFixture() (*fixture, error) {
dir, err := ioutil.TempDir("", "")
if err != nil {
- t.Fatal(err)
+ return nil, err
}
- defer os.RemoveAll(dir)
+
+ fix := fixture{dir: dir}
for _, d := range []string{"mnt", "ws", "cache"} {
if err := os.MkdirAll(filepath.Join(dir, d), 0755); err != nil {
- t.Fatal(err)
+ return nil, err
}
}
- cache, err := cache.NewCache(filepath.Join(dir, "cache"), cache.Options{})
+ fix.cache, err = cache.NewCache(filepath.Join(dir, "cache"), cache.Options{})
if err != nil {
- t.Fatal(err)
+ return nil, err
}
// Setup a fake gitiles; make sure we never talk to it.
- l, err := net.Listen("tcp", ":0")
+ fix.abortGitiles, err = net.Listen("tcp", ":0")
if err != nil {
- t.Fatal(err)
+ return nil, err
}
- go abortListener(l)
- defer l.Close()
+ go abortListener(fix.abortGitiles)
- service, err := gitiles.NewService(fmt.Sprintf("http://%s/", l.Addr()), gitiles.Options{})
+ service, err := gitiles.NewService(fmt.Sprintf("http://%s/", fix.abortGitiles.Addr()), gitiles.Options{})
if err != nil {
log.Printf("NewService: %v", err)
}
opts := fs.MultiFSOptions{}
- root := fs.NewMultiFS(service, cache, opts)
+ root := fs.NewMultiFS(service, fix.cache, opts)
fuseOpts := nodefs.NewOptions()
server, _, err := nodefs.MountRoot(filepath.Join(dir, "mnt"), root, fuseOpts)
if err != nil {
- t.Fatal(err)
+ return nil, err
}
go server.Serve()
- defer server.Unmount()
+
+ return &fix, nil
+}
+
+func TestFUSESymlink(t *testing.T) {
+ fixture, err := newFixture()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fixture.Cleanup()
// We avoid talking to gitiles by inserting entries into the
// cache manually.
- if err := cache.Tree.Add(gitID(ids[0]), &gitiles.Tree{
+ if err := fixture.cache.Tree.Add(gitID(ids[0]), &gitiles.Tree{
ID: ids[0],
Entries: []gitiles.TreeEntry{
{
@@ -100,6 +154,98 @@
Size: newInt(42),
},
{
+ Mode: 0100644,
+ Name: "link",
+ Type: "blob",
+ ID: ids[2],
+ Size: newInt(1),
+ Target: newString("non-existent"),
+ },
+ },
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ // We avoid talking to gitiles by inserting entries into the
+ // cache manually.
+ if err := fixture.cache.Tree.Add(gitID(ids[1]), &gitiles.Tree{
+ ID: ids[1],
+ Entries: []gitiles.TreeEntry{
+ {
+ Mode: 0100644,
+ Name: "a",
+ Type: "blob",
+ ID: ids[1],
+ Size: newInt(42),
+ },
+ {
+ Mode: 0100644,
+ Name: "link",
+ Type: "blob",
+ ID: ids[3],
+ Size: newInt(1),
+ Target: newString("a"),
+ },
+ },
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ for i := 0; i <= 1; i++ {
+ if err := fixture.addWorkspace(fmt.Sprintf("m%d", i), &manifest.Manifest{
+ Project: []manifest.Project{{
+ Name: "platform/project",
+ Path: "p",
+ Revision: ids[i],
+ }}}); err != nil {
+ t.Fatalf("addWorkspace(%d): %v", i, err)
+ }
+ }
+
+ ws := filepath.Join(fixture.dir, "ws")
+ m0 := filepath.Join(fixture.dir, "mnt", "m0")
+ added, changed, err := Checkout(m0, ws)
+ if err != nil {
+ t.Fatalf("Checkout m0: %v", err)
+ }
+ if len(changed) > 0 {
+ t.Errorf("got changed files %v on fresh checkout", changed)
+ }
+ if want := []string{filepath.Join(m0, "p/a"), filepath.Join(m0, "p/link")}; !reflect.DeepEqual(added, want) {
+ t.Errorf("got added %v want %v on fresh checkout", added, want)
+ }
+
+ m1 := filepath.Join(fixture.dir, "mnt", "m1")
+ added, changed, err = Checkout(m1, ws)
+ if len(added) > 0 {
+ t.Errorf("got added files %v on sync", added)
+ }
+ if want := []string{filepath.Join(m1, "p/link")}; !reflect.DeepEqual(changed, want) {
+ t.Errorf("got changed files %v, want %v", changed, want)
+ }
+}
+
+func TestBasic(t *testing.T) {
+ fixture, err := newFixture()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fixture.Cleanup()
+ dir := fixture.dir
+
+ // We avoid talking to gitiles by inserting entries into the
+ // cache manually.
+ if err := fixture.cache.Tree.Add(gitID(ids[0]), &gitiles.Tree{
+ ID: ids[0],
+ Entries: []gitiles.TreeEntry{
+ {
+ Mode: 0100644,
+ Name: "a",
+ Type: "blob",
+ ID: ids[1],
+ Size: newInt(1),
+ },
+ {
Mode: 0100644,
Name: "b/c",
Type: "blob",
@@ -110,7 +256,7 @@
}); err != nil {
t.Fatal(err)
}
- if err := cache.Tree.Add(gitID(ids[1]), &gitiles.Tree{
+ if err := fixture.cache.Tree.Add(gitID(ids[1]), &gitiles.Tree{
ID: ids[1],
Entries: []gitiles.TreeEntry{
{
@@ -118,7 +264,7 @@
Name: "a",
Type: "blob",
ID: ids[2],
- Size: newInt(42),
+ Size: newInt(1),
},
{
Mode: 0100644,
@@ -132,13 +278,14 @@
Name: "new",
Type: "blob",
ID: ids[3],
- Size: newInt(42),
+ Size: newInt(1),
},
},
}); err != nil {
t.Fatal(err)
}
- if err := cache.Tree.Add(gitID(ids[2]), &gitiles.Tree{
+
+ if err := fixture.cache.Tree.Add(gitID(ids[2]), &gitiles.Tree{
ID: ids[2],
Entries: []gitiles.TreeEntry{
{
@@ -146,21 +293,23 @@
Name: "d",
Type: "blob",
ID: ids[3],
- Size: newInt(42),
+ Size: newInt(1),
},
},
}); err != nil {
t.Fatal(err)
}
- mf1 := manifest.Manifest{
+ if err := fixture.addWorkspace("m1", &manifest.Manifest{
Project: []manifest.Project{{
Name: "platform/project",
Path: "project",
Revision: ids[0],
- }}}
+ }}}); err != nil {
+ t.Fatalf("addWorkspace(m1): %v", err)
+ }
- mf2 := manifest.Manifest{
+ if err := fixture.addWorkspace("m2", &manifest.Manifest{
Project: []manifest.Project{
{
Name: "platform/project",
@@ -171,42 +320,20 @@
Path: "sub",
Revision: ids[2],
}},
- }
-
- bytes1, err := mf1.MarshalXML()
- if err != nil {
- t.Fatal(err)
- }
- if err := ioutil.WriteFile(filepath.Join(dir, "m1.xml"), bytes1, 0644); err != nil {
- t.Fatal(err)
- }
-
- bytes2, err := mf2.MarshalXML()
- if err != nil {
- t.Fatal(err)
- }
- if err := ioutil.WriteFile(filepath.Join(dir, "m2.xml"), bytes2, 0644); err != nil {
- t.Fatal(err)
- }
-
- if err := os.Symlink(filepath.Join(dir, "m1.xml"), filepath.Join(dir, "mnt", "config", "m1")); err != nil {
- t.Fatal(err)
+ }); err != nil {
+ t.Fatalf("addWorkspace(m2): %v", err)
}
testFile := filepath.Join(dir, "mnt", "m1", "project", "b/c")
if fi, err := os.Lstat(testFile); err != nil {
t.Fatalf("Lstat(%s): %v", testFile, err)
} else if fi.Size() != 1 {
- t.Fatalf("%s has size %d", fi.Size())
- }
-
- if err := os.Symlink(filepath.Join(dir, "m2.xml"), filepath.Join(dir, "mnt", "config", "m2")); err != nil {
- t.Fatal(err)
+ t.Fatalf("%s has size %d", testFile, fi.Size())
}
ws := filepath.Join(dir, "ws")
- if _, err := Checkout(filepath.Join(dir, "mnt", "m1"), ws); err != nil {
+ if _, _, err := Checkout(filepath.Join(dir, "mnt", "m1"), ws); err != nil {
t.Fatal("Checkout m1:", err)
}
@@ -220,27 +347,25 @@
// the test setup that no blobs are shared with newly
// appearing files, or they'll be touched for being new files.
- changed, err := Checkout(filepath.Join(dir, "mnt", "m2"), ws)
+ added, changed, err := Checkout(filepath.Join(dir, "mnt", "m2"), ws)
if err != nil {
t.Fatal(err)
}
- for _, f := range []string{"project/a", "project/new"} {
- found := false
- for _, c := range changed {
- if c == filepath.Join(dir, "mnt", "m2", f) {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("file %s was not changed.", f)
- }
+ if want := []string{filepath.Join(dir, "mnt", "m2", "project/a")}; !reflect.DeepEqual(changed, want) {
+ t.Errorf("got changed %v, want %v", changed, want)
+ }
+
+ if want := []string{
+ filepath.Join(dir, "mnt", "m2", "project/new"),
+ filepath.Join(dir, "mnt", "m2", "sub/d"),
+ }; !reflect.DeepEqual(added, want) {
+ t.Errorf("got added %v, want %v", added, want)
}
if dest, err := os.Readlink(filepath.Join(ws, "sub")); err != nil {
t.Fatal(err)
} else if want := filepath.Join(dir, "mnt", "m2", "sub"); dest != want {
- t.Fatalf("got %q, want %q", dest, want)
+ t.Fatalf("Readlink(ws/sub): got %q, want %q", dest, want)
}
}
diff --git a/populate/populate.go b/populate/populate.go
index e8b91a3..37cd82c 100644
--- a/populate/populate.go
+++ b/populate/populate.go
@@ -159,16 +159,11 @@
// Returns the filenames (as relative paths) in newDir that have
// changed relative to the files in oldDir.
-func changedFiles(oldInfos map[string]*fileInfo, newInfos map[string]*fileInfo) ([]string, error) {
- var changed []string
+func changedFiles(oldInfos map[string]*fileInfo, newInfos map[string]*fileInfo) (added, changed []string, err error) {
for path, info := range newInfos {
old, ok := oldInfos[path]
if !ok {
- changed = append(changed, path)
- continue
- }
- if info.isLink {
- // TODO(hanwen): maybe we should we deref the link?
+ added = append(added, path)
continue
}
@@ -182,16 +177,17 @@
}
}
sort.Strings(changed)
- return changed, nil
+ sort.Strings(added)
+ return added, changed, nil
}
// Checkout updates a RW dir with new symlinks to the given RO dir.
// Returns the files that should be touched.
-func Checkout(ro, rw string) ([]string, error) {
+func Checkout(ro, rw string) (added, changed []string, err error) {
ro = filepath.Clean(ro)
wsName, err := clearLinks(filepath.Dir(ro), rw)
if err != nil {
- return nil, err
+ return nil, nil, err
}
oldRoot := filepath.Join(filepath.Dir(ro), wsName)
@@ -227,23 +223,27 @@
for i := 0; i < cap(errs); i++ {
err := <-errs
if err != nil {
- return nil, err
+ return nil, nil, err
}
}
if err := createLinks(roTree, rwTree, ro, rw); err != nil {
- return nil, err
+ return nil, nil, err
}
newInfos := roTree.allFiles()
- changed, err := changedFiles(oldInfos, newInfos)
+ added, changed, err = changedFiles(oldInfos, newInfos)
if err != nil {
- return nil, fmt.Errorf("changedFiles: %v", err)
+ return nil, nil, fmt.Errorf("changedFiles: %v", err)
}
for i, p := range changed {
changed[i] = filepath.Join(ro, p)
}
- return changed, nil
+ for i, p := range added {
+ added[i] = filepath.Join(ro, p)
+ }
+
+ return added, changed, nil
}