Expose SHA1s as extended attributes.

Change-Id: I71b1fbd6dd3ae475ac4f67e4a570445d2b5ddc3a
diff --git a/fs/gitilesfs.go b/fs/gitilesfs.go
index 91d1a93..616b9f7 100644
--- a/fs/gitilesfs.go
+++ b/fs/gitilesfs.go
@@ -120,7 +120,18 @@
 	return fuse.OK
 }
 
-// TODO(hanwen): implement extended attributes to read the SHA1.
+const xattrName = "user.gitsha1"
+
+func (n *gitilesNode) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
+	if attribute != xattrName {
+		return nil, fuse.ENODATA
+	}
+	return []byte(n.id.String()), fuse.OK
+}
+
+func (n *gitilesNode) ListXAttr(context *fuse.Context) (attrs []string, code fuse.Status) {
+	return []string{xattrName}, fuse.OK
+}
 
 func (n *gitilesNode) Open(flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) {
 	if n.root.handleLessIO {
@@ -216,6 +227,10 @@
 	return nodefs.NewDataFile(d.data), fuse.OK
 }
 
+func (d *dataNode) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
+	return nil, fuse.ENODATA
+}
+
 func (n *dataNode) Deletable() bool { return false }
 
 func newDataNode(c []byte) nodefs.Node {
@@ -239,6 +254,10 @@
 
 func (r *gitilesRoot) Deletable() bool { return false }
 
+func (n *gitilesRoot) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
+	return nil, fuse.ENODATA
+}
+
 func (r *gitilesRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
 	if err := r.onMount(fsConn); err != nil {
 		log.Printf("onMount: %v", err)
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index b19789c..49dc122 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -190,6 +190,23 @@
 	} else if string(got) != want {
 		t.Errorf("got %q, want %q", got, want)
 	}
+
+	data := make([]byte, 1024)
+	sz, err := syscall.Listxattr(filepath.Join(fix.mntDir, "AUTHORS"), data)
+	if err != nil {
+		t.Fatalf("Listxattr: %v", err)
+	}
+	if got, want := string(data[:sz]), xattrName+"\000"; got != want {
+		t.Errorf("got xattrs %q, want %q", got, want)
+	}
+
+	sz, err = syscall.Getxattr(filepath.Join(fix.mntDir, "AUTHORS"), xattrName, data)
+	if err != nil {
+		t.Fatalf("Getxattr: %v", err)
+	}
+	if got, want := "787d767f94fd634ed29cd69ec9f93bab2b25f5d4", string(data[:sz]); got != want {
+		t.Errorf("got %q, want %q", got, want)
+	}
 }
 
 func TestGitilesFS(t *testing.T) {
diff --git a/fs/manifestfs.go b/fs/manifestfs.go
index 3d5bce5..a6c8898 100644
--- a/fs/manifestfs.go
+++ b/fs/manifestfs.go
@@ -23,6 +23,7 @@
 	"github.com/google/gitfs/cache"
 	"github.com/google/gitfs/gitiles"
 	"github.com/google/gitfs/manifest"
+	"github.com/hanwen/go-fuse/fuse"
 	"github.com/hanwen/go-fuse/fuse/nodefs"
 	git "github.com/libgit2/git2go"
 )
@@ -45,6 +46,10 @@
 
 func (r *manifestFSRoot) Deletable() bool { return false }
 
+func (r *manifestFSRoot) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
+	return nil, fuse.ENODATA
+}
+
 // NewManifestFS creates a Manifest FS root node.
 func NewManifestFS(service *gitiles.Service, cache *cache.Cache, opts ManifestOptions) (nodefs.Node, error) {
 	xml, err := opts.Manifest.MarshalXML()
diff --git a/fs/multifs.go b/fs/multifs.go
index 57e9f98..97babe9 100644
--- a/fs/multifs.go
+++ b/fs/multifs.go
@@ -56,6 +56,10 @@
 
 func (r *multiManifestFSRoot) Deletable() bool { return false }
 
+func (r *multiManifestFSRoot) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
+	return nil, fuse.ENODATA
+}
+
 type configNode struct {
 	nodefs.Node
 	root *multiManifestFSRoot