Set timestamps for changed files in gitfs.

Change-Id: Iab14bd9c6a072e128299d4d76325afd2d9b732d9
diff --git a/cmd/gitfs-populate/main.go b/cmd/gitfs-populate/main.go
index b5dd5f9..33c00b0 100644
--- a/cmd/gitfs-populate/main.go
+++ b/cmd/gitfs-populate/main.go
@@ -16,11 +16,15 @@
 
 import (
 	"flag"
+	"fmt"
 	"io/ioutil"
 	"log"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
+	"syscall"
+	"time"
 )
 
 type repoTree struct {
@@ -174,8 +178,11 @@
 	return nil
 }
 
-// clearLinks removes all symlinks to the RO tree.
-func clearLinks(dir, mount string) error {
+// clearLinks removes all symlinks to the RO tree. It returns the workspace name that was linked before.
+func clearLinks(mount, dir string) (string, error) {
+	mount = filepath.Clean(mount)
+
+	var prefix string
 	var dirs []string
 	if err := filepath.Walk(dir, func(n string, fi os.FileInfo, err error) error {
 		if fi.Mode()&os.ModeSymlink != 0 {
@@ -184,6 +191,7 @@
 				return err
 			}
 			if strings.HasPrefix(target, mount) {
+				prefix = target
 				if err := os.Remove(n); err != nil {
 					return err
 				}
@@ -194,18 +202,80 @@
 		}
 		return nil
 	}); err != nil {
-		return err
+		return "", err
 	}
 	for _, d := range dirs {
 		// Ignore error: dir may still contain entries.
 		os.Remove(d)
 	}
-	return nil
+
+	prefix = strings.TrimPrefix(prefix, mount+"/")
+	if i := strings.Index(prefix, "/"); i != -1 {
+		prefix = prefix[:i]
+	}
+	return prefix, nil
+}
+
+func getSHA1s(dir string) (map[string]string, error) {
+	attr := "user.gitsha1"
+
+	shamap := map[string]string{}
+
+	data := make([]byte, 1024)
+
+	if err := filepath.Walk(dir, func(n string, fi os.FileInfo, err error) error {
+		if fi.Mode()&os.ModeType != 0 {
+			return nil
+		}
+		if filepath.Base(n) == ".gitid" {
+			return nil
+		}
+
+		sz, err := syscall.Getxattr(n, attr, data)
+		if err != nil {
+			return fmt.Errorf("Getxattr(%s, %s): %v", n, attr, err)
+		}
+		rel, err := filepath.Rel(dir, n)
+		if err != nil {
+			return err
+		}
+		shamap[rel] = string(data[:sz])
+		return nil
+	}); err != nil {
+		return nil, err
+	}
+
+	return shamap, nil
+}
+
+// Returns the filenames (as relative paths) in newDir that have
+// changed relative to the files in oldDir.
+func changedFiles(oldDir, newDir string) ([]string, error) {
+	// TODO(hanwen): could be parallel.
+	oldSHA1s, err := getSHA1s(oldDir)
+	if err != nil {
+		return nil, err
+	}
+	newSHA1s, err := getSHA1s(newDir)
+	if err != nil {
+		return nil, err
+	}
+
+	var changed []string
+	for k, v := range newSHA1s {
+		old, ok := oldSHA1s[k]
+		if !ok || old != v {
+			changed = append(changed, k)
+		}
+	}
+	sort.Strings(changed)
+	return changed, nil
 }
 
 // populateCheckout updates a RW dir with new symlinks to the given RO dir.
 func populateCheckout(ro, rw string) error {
-	if err := clearLinks(ro, rw); err != nil {
+	wsName, err := clearLinks(filepath.Dir(ro), rw)
+	if err != nil {
 		log.Fatal(err)
 	}
 
@@ -219,7 +289,26 @@
 		return err
 	}
 
-	return createLinks(roTree, rwTree, ro, rw)
+	if err := createLinks(roTree, rwTree, ro, rw); err != nil {
+		return err
+	}
+
+	// TODO(hanwen): can be done in parallel to the other processes.
+	oldRoot := filepath.Join(filepath.Dir(ro), wsName)
+	changed, err := changedFiles(oldRoot, ro)
+	if err != nil {
+		return fmt.Errorf("changedFiles: %v", err)
+	}
+
+	// TODO(hanwen): parallel?
+	now := time.Now()
+	for _, n := range changed {
+		if err := os.Chtimes(filepath.Join(ro, n), now, now); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
 func main() {
diff --git a/cmd/gitfs-populate/main_test.go b/cmd/gitfs-populate/main_test.go
index 436fdb0..1437b74 100644
--- a/cmd/gitfs-populate/main_test.go
+++ b/cmd/gitfs-populate/main_test.go
@@ -1,13 +1,18 @@
 package main
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"reflect"
+	"syscall"
 	"testing"
 )
 
+const attr = "user.gitsha1"
+const checksum = "3f75526aa8f01eea5d76cee10722195dc73676de"
+
 func createFSTree(names []string) (string, error) {
 	dir, err := ioutil.TempDir("", "")
 	if err != nil {
@@ -22,6 +27,9 @@
 		if err := ioutil.WriteFile(p, []byte{42}, 0644); err != nil {
 			return dir, err
 		}
+		if err := syscall.Setxattr(p, attr, []byte(checksum), 0); err != nil {
+			return dir, fmt.Errorf("Setxattr: %v", err)
+		}
 	}
 	return dir, nil
 }
@@ -99,6 +107,10 @@
 		t.Fatal("createFSTree:", err)
 	}
 
+	if err := os.Symlink(filepath.Join(dir, "ro/obsolete"), filepath.Join(dir, "rw/obsolete")); err != nil {
+		t.Errorf("Symlink: %v", err)
+	}
+
 	if err := populateCheckout(filepath.Join(dir, "ro"), filepath.Join(dir, "rw")); err != nil {
 		t.Errorf("populateCheckout: %v", err)
 	}
@@ -130,4 +142,33 @@
 		}
 	}
 
+	if fi, err := os.Lstat(filepath.Join(dir, "rw/obsolete")); err == nil {
+		t.Fatalf("obsolete symlink still there: %v", fi)
+	}
+}
+
+func TestChangedFiles(t *testing.T) {
+	dir, err := createFSTree([]string{
+		"r1/a",
+		"r1/b",
+		"r2/a",
+		"r2/b",
+		"r2/c",
+	})
+	if err != nil {
+		t.Fatalf("createFSTree: %v", err)
+	}
+
+	ck2 := "3f75526aa8f01eea5d76cee10722195dc73676df"
+	if err := syscall.Setxattr(filepath.Join(dir, "r2/b"), attr, []byte(ck2), 0); err != nil {
+		t.Fatalf("Setxattr: %v", err)
+	}
+
+	got, err := changedFiles(filepath.Join(dir, "r1"), filepath.Join(dir, "r2"))
+	if err != nil {
+		t.Fatalf("changedFiles: %v", err)
+	}
+	if want := []string{"b", "c"}; !reflect.DeepEqual(want, got) {
+		t.Errorf("got %v, want %v", got, want)
+	}
 }