git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: dturner@twopensource.com
To: git@vger.kernel.org
Cc: David Turner <dturner@twitter.com>
Subject: [PATCH 3/3] Watchman support
Date: Fri,  2 May 2014 19:14:11 -0400	[thread overview]
Message-ID: <1399072451-15561-4-git-send-email-dturner@twopensource.com> (raw)
In-Reply-To: <1399072451-15561-1-git-send-email-dturner@twopensource.com>

From: David Turner <dturner@twitter.com>

Add support for filesystem view caching and updating via Facebook's
Watchman daemon.

Signed-off-by: David Turner <dturner@twitter.com>
---
 Documentation/watchman.txt       |  27 ++
 Makefile                         |   9 +
 cache.h                          |   4 +
 config.c                         |  10 +
 configure.ac                     |   8 +
 diff-lib.c                       |  70 +++--
 dir.c                            | 210 +++++++++----
 dir.h                            |  18 +-
 environment.c                    |  18 +-
 fs_cache.c                       | 645 +++++++++++++++++++++++++++++++++++++++
 fs_cache.h                       | 138 +++++++++
 hash-io.c                        |   4 +-
 read-cache.c                     |  61 +++-
 t/t1012-read-tree-df.sh          |   2 +-
 t/t2201-add-update-typechange.sh |   5 +-
 t/t2204-add-ignored.sh           |   6 +-
 t/t6001-rev-list-graft.sh        |   2 +-
 t/t7900-watchman.sh              | 249 +++++++++++++++
 t/test-lib.sh                    |   1 +
 unpack-trees.c                   |   2 +-
 watchman-support.c               | 640 ++++++++++++++++++++++++++++++++++++++
 watchman-support.h               |  10 +
 22 files changed, 2045 insertions(+), 94 deletions(-)
 create mode 100644 Documentation/watchman.txt
 create mode 100644 fs_cache.c
 create mode 100644 fs_cache.h
 create mode 100755 t/t7900-watchman.sh
 create mode 100644 watchman-support.c
 create mode 100644 watchman-support.h

diff --git a/Documentation/watchman.txt b/Documentation/watchman.txt
new file mode 100644
index 0000000..b849e98
--- /dev/null
+++ b/Documentation/watchman.txt
@@ -0,0 +1,27 @@
+How git uses watchman
+---------------------
+
+Git status (and some other commands) have to determine which files
+have changed between the working copy and the index.  Ordinarily, this
+requires checking every file in the working directory.  But if you
+have watchman (https://github.com/facebook/watchman) installed, git
+can cache the state of the working directory and use watchman to track
+file changes, making commands like git status faster.
+
+set core.usewatchman = true to use watchman.
+You can also set
+core.watchmansynctimeout = [number of milliseconds]
+to change watchman's sync timeout; see the watchman docs for details
+on this.  You should only change this if you see watchman timeout
+error messages.
+
+Internals
+---------
+
+The filesystem cache stores information about every file in the
+working tree.  In almost every case where git calls lstat or
+opendir/readdir, the modified file cache can be consulted instead.
+
+The file system cache is stored on disk in .git/fs_cache.  It is
+stored very similarly to the index, except without path prefix
+compression.
diff --git a/Makefile b/Makefile
index 4cd642d..0c10a28 100644
--- a/Makefile
+++ b/Makefile
@@ -667,6 +667,7 @@ LIB_H += ewah/ewok.h
 LIB_H += ewah/ewok_rlw.h
 LIB_H += fetch-pack.h
 LIB_H += fmt-merge-msg.h
+LIB_H += fs_cache.h
 LIB_H += fsck.h
 LIB_H += gettext.h
 LIB_H += git-compat-util.h
@@ -808,6 +809,7 @@ LIB_OBJS += ewah/ewah_io.o
 LIB_OBJS += ewah/ewah_rlw.o
 LIB_OBJS += exec_cmd.o
 LIB_OBJS += fetch-pack.o
+LIB_OBJS += fs_cache.o
 LIB_OBJS += fsck.o
 LIB_OBJS += gettext.o
 LIB_OBJS += gpg-interface.o
@@ -1445,6 +1447,13 @@ ifdef RUNTIME_PREFIX
 	COMPAT_CFLAGS += -DRUNTIME_PREFIX
 endif
 
+ifdef USE_WATCHMAN
+	LIB_H += watchman-support.h
+	LIB_OBJS += watchman-support.o
+	EXTLIBS += $(WATCHMAN_LIBS)
+	BASIC_CFLAGS += -DUSE_WATCHMAN
+endif
+
 ifdef NO_PTHREADS
 	BASIC_CFLAGS += -DNO_PTHREADS
 else
diff --git a/cache.h b/cache.h
index c5917f4..023f9c9 100644
--- a/cache.h
+++ b/cache.h
@@ -272,6 +272,7 @@ struct index_state {
 	struct cache_entry **cache;
 	unsigned int version;
 	unsigned int cache_nr, cache_alloc, cache_changed;
+	struct fs_cache *fs_cache;
 	struct string_list *resolve_undo;
 	struct cache_tree *cache_tree;
 	struct cache_time timestamp;
@@ -352,6 +353,7 @@ static inline enum object_type object_type(unsigned int mode)
 #define DEFAULT_GIT_DIR_ENVIRONMENT ".git"
 #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY"
 #define INDEX_ENVIRONMENT "GIT_INDEX_FILE"
+#define FS_CACHE_ENVIRONMENT "GIT_FS_CACHE_FILE"
 #define GRAFT_ENVIRONMENT "GIT_GRAFT_FILE"
 #define GIT_SHALLOW_FILE_ENVIRONMENT "GIT_SHALLOW_FILE"
 #define TEMPLATE_DIR_ENVIRONMENT "GIT_TEMPLATE_DIR"
@@ -594,6 +596,8 @@ extern int check_replace_refs;
 
 extern int fsync_object_files;
 extern int core_preload_index;
+extern int core_use_watchman;
+extern int core_watchman_sync_timeout;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
 
diff --git a/config.c b/config.c
index a30cb5c..6bbdac4 100644
--- a/config.c
+++ b/config.c
@@ -854,6 +854,16 @@ static int git_default_core_config(const char *var, const char *value)
 		return 0;
 	}
 
+	if (!strcmp(var, "core.usewatchman")) {
+		core_use_watchman = git_config_bool(var, value);
+		return 0;
+	}
+
+	if (!strcmp(var, "core.watchmansynctimeout")) {
+		core_watchman_sync_timeout = git_config_int(var, value);
+		return 0;
+	}
+
 	if (!strcmp(var, "core.createobject")) {
 		if (!strcmp(value, "rename"))
 			object_creation_mode = OBJECT_CREATION_USES_RENAMES;
diff --git a/configure.ac b/configure.ac
index b711254..76fcb83 100644
--- a/configure.ac
+++ b/configure.ac
@@ -962,6 +962,14 @@ GIT_CONF_SUBST([NO_INITGROUPS])
 #
 # Define NO_ICONV if your libc does not properly support iconv.
 
+# Check for watchman client library
+
+AC_CHECK_LIB([watchman], [watchman_connect],
+       [WATCHMAN_LIBS="-lwatchman"
+	USE_WATCHMAN=YesPlease],
+       [USE_WATCHMAN=])
+GIT_CONF_SUBST([WATCHMAN_LIBS])
+GIT_CONF_SUBST([USE_WATCHMAN])
 
 ## Other checks.
 # Define USE_PIC if you need the main git objects to be built with -fPIC
diff --git a/diff-lib.c b/diff-lib.c
index 0448729..bbeca00 100644
--- a/diff-lib.c
+++ b/diff-lib.c
@@ -2,6 +2,7 @@
  * Copyright (C) 2005 Junio C Hamano
  */
 #include "cache.h"
+#include "fs_cache.h"
 #include "quote.h"
 #include "commit.h"
 #include "diff.h"
@@ -17,24 +18,8 @@
  * diff-files
  */
 
-/*
- * Has the work tree entity been removed?
- *
- * Return 1 if it was removed from the work tree, 0 if an entity to be
- * compared with the cache entry ce still exists (the latter includes
- * the case where a directory that is not a submodule repository
- * exists for ce that is a submodule -- it is a submodule that is not
- * checked out).  Return negative for an error.
- */
-static int check_removed(const struct cache_entry *ce, struct stat *st)
+static int check_gitlink(const struct cache_entry *ce, struct stat *st)
 {
-	if (lstat(ce->name, st) < 0) {
-		if (errno != ENOENT && errno != ENOTDIR)
-			return -1;
-		return 1;
-	}
-	if (has_symlink_leading_path(ce->name, ce_namelen(ce)))
-		return 1;
 	if (S_ISDIR(st->st_mode)) {
 		unsigned char sub[20];
 
@@ -56,6 +41,57 @@ static int check_removed(const struct cache_entry *ce, struct stat *st)
 	return 0;
 }
 
+static int fs_cache_check_removed(const struct fs_cache *fs_cache, const struct cache_entry *ce, struct stat *st)
+{
+	struct fsc_entry *fe;
+
+	fe = fs_cache_file_exists(fs_cache, ce->name, ce_namelen(ce));
+	if (!fe) {
+		return 1;
+	}
+	if (fe_deleted(fe)) {
+		return 1;
+	}
+
+	fe_to_stat(fe, st);
+
+/* This should not be necessary, because the fs_cache does not store
+   children of symlinked directories.  TODO: ensure this
+	if (has_symlink_leading_path(ce->name, ce_namelen(ce)))
+		return 1;
+*/
+	if (check_gitlink(ce, st))
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Has the work tree entity been removed?
+ *
+ * Return 1 if it was removed from the work tree, 0 if an entity to be
+ * compared with the cache entry ce still exists (the latter includes
+ * the case where a directory that is not a submodule repository
+ * exists for ce that is a submodule -- it is a submodule that is not
+ * checked out).  Return negative for an error.
+ */
+static int check_removed(const struct cache_entry *ce, struct stat *st)
+{
+	if (the_index.fs_cache)
+		return fs_cache_check_removed(the_index.fs_cache, ce, st);
+
+	if (lstat(ce->name, st) < 0) {
+		if (errno != ENOENT && errno != ENOTDIR)
+			return -1;
+		return 1;
+	}
+	if (has_symlink_leading_path(ce->name, ce_namelen(ce)))
+		return 1;
+	if (check_gitlink(ce, st))
+		return 1;
+	return 0;
+}
+
 /*
  * Has a file changed or has a submodule new commits or a dirty work tree?
  *
diff --git a/dir.c b/dir.c
index eb6f581..41258ce 100644
--- a/dir.c
+++ b/dir.c
@@ -8,6 +8,7 @@
  *		 Junio Hamano, 2005-2006
  */
 #include "cache.h"
+#include "fs_cache.h"
 #include "dir.h"
 #include "refs.h"
 #include "wildmatch.h"
@@ -33,8 +34,8 @@ enum path_treatment {
 
 static enum path_treatment read_directory_recursive(struct dir_struct *dir,
 	const char *path, int len,
-	int check_only, const struct path_simplify *simplify);
-static int get_dtype(struct dirent *de, const char *path, int len);
+	int check_only, const struct path_simplify *simplify,
+	struct fsc_entry *fe);
 
 /* helper string functions with support for the ignore_case flag */
 int strcmp_icase(const char *a, const char *b)
@@ -179,7 +180,8 @@ int fill_directory(struct dir_struct *dir, const struct pathspec *pathspec)
 	len = common_prefix_len(pathspec);
 
 	/* Read the directory and prune it */
-	read_directory(dir, pathspec->nr ? pathspec->_raw[0] : "", len, pathspec);
+	read_directory(&the_index, dir, pathspec->nr ? pathspec->_raw[0] : "",
+		       len, pathspec);
 	return len;
 }
 
@@ -536,7 +538,7 @@ int add_excludes_from_file_to_list(const char *fname,
 	size_t size = 0;
 	char *buf, *entry;
 
-	fd = open(fname, O_RDONLY);
+	fd = fs_cache_open(the_index.fs_cache, fname, O_RDONLY);
 	if (fd < 0 || fstat(fd, &st) < 0) {
 		if (errno != ENOENT)
 			warn_on_inaccessible(fname);
@@ -597,6 +599,8 @@ struct exclude_list *add_exclude_list(struct dir_struct *dir,
 	el = &group->el[group->nr++];
 	memset(el, 0, sizeof(*el));
 	el->src = src;
+	if (group_type == EXC_FILE)
+		dir->flags &= ~DIR_STD_EXCLUDES;
 	return el;
 }
 
@@ -609,6 +613,7 @@ void add_excludes_from_file(struct dir_struct *dir, const char *fname)
 	el = add_exclude_list(dir, EXC_FILE, fname);
 	if (add_excludes_from_file_to_list(fname, "", 0, el, 0) < 0)
 		die("cannot use %s as an exclude file", fname);
+	dir->flags &= ~DIR_STD_EXCLUDES;
 }
 
 int match_basename(const char *basename, int basenamelen,
@@ -763,7 +768,9 @@ static struct exclude *last_exclude_matching_from_lists(struct dir_struct *dir,
 	int i, j;
 	struct exclude_list_group *group;
 	struct exclude *exclude;
-	for (i = EXC_CMDL; i <= EXC_FILE; i++) {
+	int last = dir->flags & DIR_EXCLUDE_CMDL_ONLY ? EXC_CMDL : EXC_FILE;
+
+	for (i = EXC_CMDL; i <= last; i++) {
 		group = &dir->exclude_list_group[i];
 		for (j = group->nr - 1; j >= 0; j--) {
 			exclude = last_exclude_matching_from_list(
@@ -852,6 +859,7 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
 
 		/* Try to read per-directory file unless path is too long */
 		if (dir->exclude_per_dir &&
+		    !(dir->flags & DIR_EXCLUDE_CMDL_ONLY) &&
 		    stk->baselen + strlen(dir->exclude_per_dir) < PATH_MAX) {
 			strcpy(dir->basebuf + stk->baselen,
 					dir->exclude_per_dir);
@@ -910,6 +918,17 @@ int is_excluded(struct dir_struct *dir, const char *pathname, int *dtype_p)
 	return 0;
 }
 
+static int fs_cache_is_excluded(struct dir_struct *dir, const char *pathname, int *dtype_p, struct fsc_entry *fe)
+{
+	struct exclude *exclude;
+	exclude = last_exclude_matching(dir, pathname, dtype_p);
+	if (exclude)
+		return exclude->flags & EXC_FLAG_NEGATIVE ? 0 : 1;
+	if (dir->flags & DIR_STD_EXCLUDES && fe)
+		return fe_excluded(fe);
+	return 0;
+}
+
 static struct dir_entry *dir_entry_new(const char *pathname, int len)
 {
 	struct dir_entry *ent;
@@ -1047,7 +1066,7 @@ static enum exist_status directory_exists_in_index(const char *dirname, int len)
  */
 static enum path_treatment treat_directory(struct dir_struct *dir,
 	const char *dirname, int len, int exclude,
-	const struct path_simplify *simplify)
+	const struct path_simplify *simplify, struct fsc_entry *fe)
 {
 	/* The "len-1" is to strip the final '/' */
 	switch (directory_exists_in_index(dirname, len-1)) {
@@ -1073,7 +1092,7 @@ static enum path_treatment treat_directory(struct dir_struct *dir,
 	if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
 		return exclude ? path_excluded : path_untracked;
 
-	return read_directory_recursive(dir, dirname, len, 1, simplify);
+	return read_directory_recursive(dir, dirname, len, 1, simplify, fe);
 }
 
 /*
@@ -1167,7 +1186,7 @@ static int get_index_dtype(const char *path, int len)
 	return DT_UNKNOWN;
 }
 
-static int get_dtype(struct dirent *de, const char *path, int len)
+int get_dtype(struct dirent *de, const char *path, int len)
 {
 	int dtype = de ? DTYPE(de) : DT_UNKNOWN;
 	struct stat st;
@@ -1191,7 +1210,8 @@ static int get_dtype(struct dirent *de, const char *path, int len)
 static enum path_treatment treat_one_path(struct dir_struct *dir,
 					  struct strbuf *path,
 					  const struct path_simplify *simplify,
-					  int dtype, struct dirent *de)
+					  int dtype, struct dirent *de,
+					  struct fsc_entry *fe)
 {
 	int exclude;
 	int has_path_in_index = !!cache_file_exists(path->buf, path->len, ignore_case);
@@ -1227,7 +1247,7 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
 	    (directory_exists_in_index(path->buf, path->len) == index_nonexistent))
 		return path_none;
 
-	exclude = is_excluded(dir, path->buf, &dtype);
+	exclude = fs_cache_is_excluded(dir, path->buf, &dtype, fe);
 
 	/*
 	 * Excluded? If we don't explicitly want to show
@@ -1242,7 +1262,7 @@ static enum path_treatment treat_one_path(struct dir_struct *dir,
 	case DT_DIR:
 		strbuf_addch(path, '/');
 		return treat_directory(dir, path->buf, path->len, exclude,
-			simplify);
+			simplify, fe);
 	case DT_REG:
 	case DT_LNK:
 		return exclude ? path_excluded : path_untracked;
@@ -1253,7 +1273,8 @@ static enum path_treatment treat_path(struct dir_struct *dir,
 				      struct dirent *de,
 				      struct strbuf *path,
 				      int baselen,
-				      const struct path_simplify *simplify)
+				      const struct path_simplify *simplify,
+				      struct fsc_entry *fe)
 {
 	int dtype;
 
@@ -1265,7 +1286,59 @@ static enum path_treatment treat_path(struct dir_struct *dir,
 		return path_none;
 
 	dtype = DTYPE(de);
-	return treat_one_path(dir, path, simplify, dtype, de);
+	return treat_one_path(dir, path, simplify, dtype, de, fe);
+}
+
+static int handle(struct dir_struct *dir, const char *base, int baselen,
+		  int check_only, const struct path_simplify *simplify,
+		  struct dirent *de, enum path_treatment *dir_state,
+		  struct strbuf *path, struct fsc_entry *fe)
+{
+	enum path_treatment state, subdir_state;
+
+	/* check how the file or directory should be treated */
+	state = treat_path(dir, de, path, baselen, simplify, fe);
+
+	if (state > *dir_state)
+		*dir_state = state;
+
+	/* recurse into subdir if instructed by treat_path */
+	if (state == path_recurse) {
+		subdir_state = read_directory_recursive(dir, path->buf,
+			path->len, check_only, simplify, fe);
+		if (subdir_state > *dir_state)
+			*dir_state = subdir_state;
+	}
+
+	if (check_only) {
+		/* abort early if maximum state has been reached */
+		if (*dir_state == path_untracked)
+			return 1;
+		/* skip the dir_add_* part */
+		return 0;
+	}
+
+	/* add the path to the appropriate result list */
+	switch (state) {
+	case path_excluded:
+		if (dir->flags & DIR_SHOW_IGNORED)
+			dir_add_name(dir, path->buf, path->len);
+		else if ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
+			 ((dir->flags & DIR_COLLECT_IGNORED) &&
+			  exclude_matches_pathspec(path->buf, path->len,
+				simplify)))
+			dir_add_ignored(dir, path->buf, path->len);
+		break;
+
+	case path_untracked:
+		if (!(dir->flags & DIR_SHOW_IGNORED))
+			dir_add_name(dir, path->buf, path->len);
+		break;
+
+	default:
+		break;
+	}
+	return 0;
 }
 
 /*
@@ -1282,63 +1355,44 @@ static enum path_treatment treat_path(struct dir_struct *dir,
 static enum path_treatment read_directory_recursive(struct dir_struct *dir,
 				    const char *base, int baselen,
 				    int check_only,
-				    const struct path_simplify *simplify)
+				    const struct path_simplify *simplify,
+				    struct fsc_entry *parent)
 {
 	DIR *fdir;
-	enum path_treatment state, subdir_state, dir_state = path_none;
-	struct dirent *de;
+	enum path_treatment dir_state = path_none;
 	struct strbuf path = STRBUF_INIT;
 
 	strbuf_add(&path, base, baselen);
 
-	fdir = opendir(path.len ? path.buf : ".");
-	if (!fdir)
-		goto out;
-
-	while ((de = readdir(fdir)) != NULL) {
-		/* check how the file or directory should be treated */
-		state = treat_path(dir, de, &path, baselen, simplify);
-		if (state > dir_state)
-			dir_state = state;
-
-		/* recurse into subdir if instructed by treat_path */
-		if (state == path_recurse) {
-			subdir_state = read_directory_recursive(dir, path.buf,
-				path.len, check_only, simplify);
-			if (subdir_state > dir_state)
-				dir_state = subdir_state;
-		}
+	if (the_index.fs_cache) {
+		struct fsc_entry *fe;
 
-		if (check_only) {
-			/* abort early if maximum state has been reached */
-			if (dir_state == path_untracked)
+		if (!parent)
+			goto out;
+
+		for (fe = parent->first_child; fe; fe = fe->next_sibling) {
+			struct dirent de;
+			if (fe_deleted(fe))
+				continue;
+			de.d_ino = -1;
+			de.d_type = fe_dtype(fe);
+			strcpy(de.d_name, basename(fe->path));
+			if (handle(dir, base, baselen, check_only, simplify, &de, &dir_state, &path, fe))
 				break;
-			/* skip the dir_add_* part */
-			continue;
 		}
+	} else {
+		struct dirent *de;
+		fdir = opendir(path.len ? path.buf : ".");
+		if (!fdir)
+			goto out;
 
-		/* add the path to the appropriate result list */
-		switch (state) {
-		case path_excluded:
-			if (dir->flags & DIR_SHOW_IGNORED)
-				dir_add_name(dir, path.buf, path.len);
-			else if ((dir->flags & DIR_SHOW_IGNORED_TOO) ||
-				((dir->flags & DIR_COLLECT_IGNORED) &&
-				exclude_matches_pathspec(path.buf, path.len,
-					simplify)))
-				dir_add_ignored(dir, path.buf, path.len);
-			break;
-
-		case path_untracked:
-			if (!(dir->flags & DIR_SHOW_IGNORED))
-				dir_add_name(dir, path.buf, path.len);
-			break;
-
-		default:
-			break;
+		while ((de = readdir(fdir)) != NULL) {
+			if (handle(dir, base, baselen, check_only, simplify, de, &dir_state, &path, NULL))
+				break;
 		}
+		closedir(fdir);
 	}
-	closedir(fdir);
+
  out:
 	strbuf_release(&path);
 
@@ -1383,7 +1437,8 @@ static void free_simplify(struct path_simplify *simplify)
 
 static int treat_leading_path(struct dir_struct *dir,
 			      const char *path, int len,
-			      const struct path_simplify *simplify)
+			      const struct path_simplify *simplify,
+			      struct fsc_entry *fe)
 {
 	struct strbuf sb = STRBUF_INIT;
 	int baselen, rc = 0;
@@ -1410,7 +1465,7 @@ static int treat_leading_path(struct dir_struct *dir,
 		if (simplify_away(sb.buf, sb.len, simplify))
 			break;
 		if (treat_one_path(dir, &sb, simplify,
-				   DT_DIR, NULL) == path_none)
+				   DT_DIR, NULL, fe) == path_none)
 			break; /* do not recurse into it */
 		if (len <= baselen) {
 			rc = 1;
@@ -1422,9 +1477,13 @@ static int treat_leading_path(struct dir_struct *dir,
 	return rc;
 }
 
-int read_directory(struct dir_struct *dir, const char *path, int len, const struct pathspec *pathspec)
+
+int read_directory(struct index_state *istate, struct dir_struct *dir,
+		   const char *path, int len, const struct pathspec *pathspec)
 {
 	struct path_simplify *simplify;
+	int saved_flags = dir->flags;
+	struct fsc_entry *fe = NULL;
 
 	/*
 	 * Check out create_simplify()
@@ -1448,11 +1507,32 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
 	 * create_simplify().
 	 */
 	simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
-	if (!len || treat_leading_path(dir, path, len, simplify))
-		read_directory_recursive(dir, path, len, 0, simplify);
+
+	/*
+	  Check for standard excludes.
+	  Standard excludes means: exclude_per_dir
+	 */
+	if (!dir->exclude_per_dir || strcmp(dir->exclude_per_dir, ".gitignore"))
+		dir->flags &= ~DIR_STD_EXCLUDES;
+
+	if (istate->fs_cache && dir->flags & DIR_STD_EXCLUDES) {
+		dir->flags |= DIR_EXCLUDE_CMDL_ONLY;
+	}
+
+	if (istate->fs_cache) {
+		int len_no_slash = len;
+		if (len && path[len - 1] == '/')
+			len_no_slash --;
+		fe = fs_cache_file_exists(istate->fs_cache, path, len_no_slash);
+	}
+
+	if (!len || treat_leading_path(dir, path, len, simplify, fe)) {
+		read_directory_recursive(dir, path, len, 0, simplify, fe);
+	}
 	free_simplify(simplify);
 	qsort(dir->entries, dir->nr, sizeof(struct dir_entry *), cmp_name);
 	qsort(dir->ignored, dir->ignored_nr, sizeof(struct dir_entry *), cmp_name);
+	dir->flags = saved_flags;
 	return dir->nr;
 }
 
@@ -1608,6 +1688,7 @@ void setup_standard_excludes(struct dir_struct *dir)
 {
 	const char *path;
 	char *xdg_path;
+	int previously_empty;
 
 	dir->exclude_per_dir = ".gitignore";
 	path = git_path("info/exclude");
@@ -1615,10 +1696,13 @@ void setup_standard_excludes(struct dir_struct *dir)
 		home_config_paths(NULL, &xdg_path, "ignore");
 		excludes_file = xdg_path;
 	}
+	previously_empty = dir->exclude_list_group[EXC_FILE].nr == 0;
 	if (!access_or_warn(path, R_OK, 0))
 		add_excludes_from_file(dir, path);
 	if (excludes_file && !access_or_warn(excludes_file, R_OK, 0))
 		add_excludes_from_file(dir, excludes_file);
+	if (previously_empty)
+		dir->flags |= DIR_STD_EXCLUDES;
 }
 
 int remove_path(const char *name)
diff --git a/dir.h b/dir.h
index 55e5345..6453faf 100644
--- a/dir.h
+++ b/dir.h
@@ -81,7 +81,18 @@ struct dir_struct {
 		DIR_NO_GITLINKS = 1<<3,
 		DIR_COLLECT_IGNORED = 1<<4,
 		DIR_SHOW_IGNORED_TOO = 1<<5,
-		DIR_COLLECT_KILLED_ONLY = 1<<6
+		DIR_COLLECT_KILLED_ONLY = 1<<6,
+		/*
+		 * Whether the standard excludes are the only file
+		 * excludes which have been set up (if so, we can use
+		 * the fs_cache to optimize is_excluded).
+		 */
+		DIR_STD_EXCLUDES = 1<<7,
+		/*
+		 * Excludes should only check the command-line (for
+		 * use with fs_cache)
+		 */
+		DIR_EXCLUDE_CMDL_ONLY = 1<<8
 	} flags;
 	struct dir_entry **entries;
 	struct dir_entry **ignored;
@@ -138,7 +149,7 @@ extern int match_pathspec(const struct pathspec *pathspec,
 extern int within_depth(const char *name, int namelen, int depth, int max_depth);
 
 extern int fill_directory(struct dir_struct *dir, const struct pathspec *pathspec);
-extern int read_directory(struct dir_struct *, const char *path, int len, const struct pathspec *pathspec);
+extern int read_directory(struct index_state *index, struct dir_struct *, const char *path, int len, const struct pathspec *pathspec);
 
 extern int is_excluded_from_list(const char *pathname, int pathlen, const char *basename,
 				 int *dtype, struct exclude_list *el);
@@ -223,4 +234,7 @@ static inline int dir_path_match(const struct dir_entry *ent,
 			      has_trailing_dir);
 }
 
+int get_dtype(struct dirent *de, const char *path, int len);
+
+
 #endif
diff --git a/environment.c b/environment.c
index 5c4815d..060e473 100644
--- a/environment.c
+++ b/environment.c
@@ -73,6 +73,10 @@ char comment_line_char = '#';
 /* Parallel index stat data preload? */
 int core_preload_index = 0;
 
+/* Use Watchman for faster status queries */
+int core_use_watchman = 0;
+int core_watchman_sync_timeout = 300;
+
 /* This is set by setup_git_dir_gently() and/or git_default_config() */
 char *git_work_tree_cfg;
 static char *work_tree;
@@ -81,7 +85,7 @@ static const char *namespace;
 static size_t namespace_len;
 
 static const char *git_dir;
-static char *git_object_dir, *git_index_file, *git_graft_file;
+static char *git_object_dir, *git_index_file, *git_graft_file, *git_fs_cache_file;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -143,6 +147,11 @@ static void setup_git_env(void)
 		git_index_file = xmalloc(strlen(git_dir) + 7);
 		sprintf(git_index_file, "%s/index", git_dir);
 	}
+	git_fs_cache_file = getenv(FS_CACHE_ENVIRONMENT);
+	if (!git_fs_cache_file) {
+		git_fs_cache_file = xmalloc(strlen(git_dir) + 10);
+		sprintf(git_fs_cache_file, "%s/fs_cache", git_dir);
+	}
 	git_graft_file = getenv(GRAFT_ENVIRONMENT);
 	if (!git_graft_file)
 		git_graft_file = git_pathdup("info/grafts");
@@ -266,6 +275,13 @@ char *get_graft_file(void)
 	return git_graft_file;
 }
 
+char *get_fs_cache_file(void)
+{
+	if (!git_fs_cache_file)
+		setup_git_env();
+	return git_fs_cache_file;
+}
+
 int set_git_dir(const char *path)
 {
 	if (setenv(GIT_DIR_ENVIRONMENT, path, 1))
diff --git a/fs_cache.c b/fs_cache.c
new file mode 100644
index 0000000..3702186
--- /dev/null
+++ b/fs_cache.c
@@ -0,0 +1,645 @@
+#include "cache.h"
+#include "fs_cache.h"
+#include "strbuf.h"
+#include "hashmap.h"
+#include "hash-io.h"
+
+#define FS_CACHE_SIGNATURE 0x4D4F4443	/* "MODC" */
+
+static int fe_entry_cmp(const struct fsc_entry *f1,
+			const struct fsc_entry *f2,
+			const char *name)
+{
+	if (f1->pathlen != f2->pathlen)
+		return 1;
+	name = name ? name : f2->path;
+	return ignore_case ? strncasecmp(f1->path, name, f1->pathlen) :
+		strncmp(f1->path, name, f1->pathlen);
+
+}
+
+unsigned char fe_dtype(struct fsc_entry *file)
+{
+	if (fe_is_reg(file)) {
+		return DT_REG;
+	}
+	if (fe_is_dir(file)) {
+		return DT_DIR;
+	}
+	if (fe_is_lnk(file)) {
+		return DT_LNK;
+	}
+	return DT_UNKNOWN;
+}
+
+#define FS_CACHE_FORMAT_LB 1
+#define FS_CACHE_FORMAT_UB 2
+
+static int verify_hdr(struct fs_cache_header *hdr, unsigned long size)
+{
+	vmac_ctx_t c;
+	unsigned char sha1[20];
+	int hdr_version;
+
+	if (hdr->hdr_signature != htonl(FS_CACHE_SIGNATURE)) {
+		warning("bad fs_cache signature");
+		return -1;
+	}
+	hdr_version = ntohl(hdr->hdr_version);
+	if (hdr_version < FS_CACHE_FORMAT_LB || FS_CACHE_FORMAT_UB < hdr_version) {
+		warning("bad fs_cache version %d", hdr_version);
+		return -1;
+	}
+
+	unsigned char *key = (unsigned char *)"abcdefghijklmnop";
+	vmac_set_key(key, &c);
+	vmac_update_unaligned(hdr, size - 20, &c);
+	vmac_final(sha1, &c);
+	if (hashcmp(sha1, (unsigned char *)hdr + size - 20)) {
+		warning("bad fs_cache file vmac signature");
+		return -1;
+	}
+
+	return 0;
+}
+
+static struct fsc_entry *create_from_disk(struct fs_cache *fs_cache, struct ondisk_fsc_entry *disk_fe, unsigned long *consumed)
+{
+	struct fsc_entry *fe;
+	int pathlen = strlen(disk_fe->path);
+
+	fe = obstack_alloc(&fs_cache->obs, sizeof(*fe) + pathlen + 1);
+
+	fe->size = ntohl(disk_fe->size);
+	fe->mode = ntohl(disk_fe->mode);
+	fe->flags = ntohl(disk_fe->flags);
+
+	fe->ctime.sec = ntohl(disk_fe->ctime.sec);
+	fe->mtime.sec = ntohl(disk_fe->mtime.sec);
+	fe->ctime.nsec = ntohl(disk_fe->ctime.nsec);
+	fe->mtime.nsec = ntohl(disk_fe->mtime.nsec);
+
+	fe->ino = ntohl(disk_fe->ino);
+	fe->dev = ntohl(disk_fe->dev);
+
+	fe->uid = ntohl(disk_fe->uid);
+	fe->gid = ntohl(disk_fe->gid);
+
+	fe->parent = NULL;
+	fe->first_child = NULL;
+	fe->next_sibling = NULL;
+	memcpy(fe->path, disk_fe->path, pathlen + 1);
+	fe->pathlen = pathlen;
+
+	hashmap_entry_init(fe, memihash(fe->path, pathlen));
+	*consumed = sizeof(*disk_fe) + pathlen + 1;
+	return fe;
+}
+
+static void copy_fs_cache_entry_to_ondisk(
+	struct ondisk_fsc_entry *ondisk,
+	struct fsc_entry *fe)
+{
+
+	ondisk->size = htonl(fe->size);
+	ondisk->mode = htonl(fe->mode);
+	ondisk->flags = htonl(fe->flags & ~FE_NEW);
+
+	ondisk->ctime.sec = htonl(fe->ctime.sec);
+	ondisk->mtime.sec = htonl(fe->mtime.sec);
+	ondisk->ctime.nsec = htonl(fe->ctime.nsec);
+	ondisk->mtime.nsec = htonl(fe->mtime.nsec);
+
+	ondisk->ino = htonl(fe->ino);
+	ondisk->dev = htonl(fe->dev);
+
+	ondisk->uid = htonl(fe->uid);
+	ondisk->gid = htonl(fe->gid);
+
+	memcpy(ondisk->path, fe->path, fe->pathlen + 1);
+
+}
+
+static int fe_write_entry(struct fsc_entry *fe, int fd, struct hash_context *context)
+{
+	int result;
+	static struct ondisk_fsc_entry *ondisk = NULL;
+	static size_t max_size = sizeof(*ondisk) + 1 + PATH_MAX;
+	size_t size;
+
+	size = sizeof(*ondisk) + fe->pathlen + 1;
+	if (size > max_size) {
+		max_size = size;
+		if (ondisk) {
+			ondisk = xrealloc(ondisk, max_size);
+			memset(ondisk, 0, max_size);
+		}
+	}
+
+
+	if (!ondisk)
+		ondisk = xcalloc(1, max_size);
+
+	copy_fs_cache_entry_to_ondisk(ondisk, fe);
+
+	result = write_with_hash(context, fd, ondisk, size);
+
+	return result ? -1 : 0;
+}
+
+static int fe_write_entry_recursive(struct fsc_entry *fe, int fd, struct hash_context *c)
+{
+	if (fe_write_entry(fe, fd, c))
+		return error("failed to write some file of fs_cache");
+	fe = fe->first_child;
+	while (fe) {
+		fe_write_entry_recursive(fe, fd, c);
+		fe = fe->next_sibling;
+	}
+
+	return 0;
+}
+
+static char *xstrcpy(char *dest, const char *src)
+{
+	while ((*dest++ = *src++)) {
+	}
+
+	return dest;
+}
+
+int write_fs_cache(struct fs_cache *fs_cache)
+{
+	struct hash_context c;
+	struct fs_cache_header *hdr;
+	int hdr_size;
+	struct stat st;
+	int fd;
+	const char *path;
+	char *cur;
+	int string_size;
+
+	path = get_fs_cache_file();
+
+	fd = open(path, O_WRONLY|O_TRUNC|O_CREAT, 0666);
+	if (fd < 0)
+		die_errno("failed to open fs_cache file %s", path);
+
+	string_size = strlen(fs_cache->last_update) +
+		strlen(fs_cache->repo_path) +
+		strlen(fs_cache->excludes_file) + 3;
+
+	hdr_size = sizeof(*hdr) + string_size;
+	hdr = xmalloc(hdr_size);
+	hdr->hdr_signature = htonl(FS_CACHE_SIGNATURE);
+	hdr->hdr_version = htonl(fs_cache->version);
+	hdr->hdr_entries = htonl(fs_cache->nr);
+	hdr->flags = htonl(fs_cache->flags);
+	hashcpy(hdr->git_excludes_sha1, fs_cache->git_excludes_sha1);
+	hashcpy(hdr->user_excludes_sha1, fs_cache->user_excludes_sha1);
+	cur = xstrcpy(hdr->strings, fs_cache->last_update);
+	cur = xstrcpy(cur, fs_cache->repo_path);
+	cur = xstrcpy(cur, fs_cache->excludes_file);
+
+	hash_context_init(&c, HASH_IO_VMAC);
+
+	if (write_with_hash(&c, fd, hdr, hdr_size) < 0)
+		die_errno("failed to write header of fs_cache");
+
+	fe_write_entry_recursive(fs_cache_file_exists(fs_cache, "", 0), fd, &c);
+	if (write_with_hash_flush(&c, fd) || fstat(fd, &st))
+		return error("Failed to flush/fstat fs_cache file");
+
+	hash_context_release(&c);
+	free(hdr);
+	return 0;
+}
+
+void *mmap_fs_cache(size_t *mmap_size) {
+	struct stat st;
+	void *mmap;
+	const char *path = get_fs_cache_file();
+	int fd = open(path, O_RDONLY);
+	if (fd < 0) {
+		if (errno == ENOENT)
+			return NULL;
+		die_errno("fs_cache file open failed");
+	}
+
+	if (fstat(fd, &st))
+		die_errno("cannot stat the open fs_cache");
+
+	*mmap_size = xsize_t(st.st_size);
+	if (*mmap_size < sizeof(struct fs_cache_header) + 20) {
+		warning("fs_cache file smaller than expected");
+		return NULL;
+	}
+
+	mmap = xmmap(NULL, *mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+	if (mmap == MAP_FAILED)
+		die_errno("unable to map fs_cache file");
+	close(fd);
+	return mmap;
+}
+
+/* Loading the fs_cache can take some time, and we might want to thread
+ * it with other loads; we need the last-update time and the repo path
+ * to check whether this is a good idea, so this function will preload
+ * it.  Note that the caller must free the returned strings.
+ */
+void fs_cache_preload_metadata(char **last_update, char **repo_path) {
+	size_t mmap_size;
+	void *mmap;
+	struct fs_cache_header *hdr;
+	int version;
+
+	mmap = mmap_fs_cache(&mmap_size);
+	if (!mmap) {
+		*last_update = *repo_path = NULL;
+		return;
+	}
+	hdr = mmap;
+	version = ntohl(hdr->hdr_version);
+	if (version < FS_CACHE_FORMAT_LB || FS_CACHE_FORMAT_UB < version) {
+		warning("bad fs_cache version %d", version);
+		goto unmap;
+	}
+
+	*last_update = xstrdup(hdr->strings);
+	*repo_path = xstrdup(hdr->strings + strlen(*last_update) + 1);
+
+unmap:
+	munmap(mmap, mmap_size);
+}
+
+static void write_fs_cache_if_necessary(void)
+{
+	struct fs_cache *fs_cache = the_index.fs_cache;
+	if (fs_cache && fs_cache->needs_write && fs_cache->fully_loaded) {
+		write_fs_cache(fs_cache);
+		the_index.fs_cache = 0;
+	}
+}
+
+static void fe_set_parent(struct fsc_entry *fe, struct fsc_entry *parent)
+{
+	fe->parent = parent;
+	fe->next_sibling = fe->parent->first_child;
+	fe->parent->first_child = fe;
+}
+
+void set_up_parent(struct fs_cache *fs_cache, struct fsc_entry *fe)
+{
+	char *last_slash;
+	int parent_len;
+	struct fsc_entry *parent;
+	if (fe->pathlen == 0)
+		return;
+
+	last_slash = strrchr(fe->path, '/');
+
+	if (last_slash) {
+		parent_len = last_slash - fe->path;
+	} else {
+		parent_len = 0;
+	}
+
+	parent = fs_cache_file_exists(fs_cache, fe->path, parent_len);
+	if (!parent) {
+		die("Missing parent directory for %s", fe->path);
+	}
+	fe_set_parent(fe, parent);
+}
+
+static char *read_string(char **out, char *in) {
+	int len = strlen(in);
+	*out = xstrdup(in);
+	return in + len + 1;
+}
+
+/* Load the modified file cache from disk.  If the cache is corrupt,
+ * prints a warning and returns NULL; we can safely recreate it.  If
+ * the cache is missing, also returns NULL.  If there is some other
+ * problem reading the cache (say it's read-only, or we get an io
+ * error), die with an error message. */
+struct fs_cache *read_fs_cache(void)
+{
+	struct fs_cache *fs_cache;
+	struct fs_cache_header *hdr;
+	int i;
+	unsigned int nr;
+	void *mmap;
+	void *mmap_cur;
+	size_t mmap_size;
+
+	mmap = mmap_fs_cache(&mmap_size);
+	if (!mmap) {
+		return NULL;
+	}
+
+	hdr = mmap;
+	if (verify_hdr(hdr, mmap_size) < 0)
+		goto unmap;
+
+	fs_cache = xcalloc(1, sizeof(*fs_cache));
+	obstack_init(&fs_cache->obs);
+	nr = ntohl(hdr->hdr_entries);
+	fs_cache->flags = ntohl(hdr->flags);
+	fs_cache->version = ntohl(hdr->hdr_version);
+	hashmap_init(&fs_cache->paths, (hashmap_cmp_fn) fe_entry_cmp, nr);
+	fs_cache->nr = 0;
+	hashcpy(fs_cache->git_excludes_sha1, hdr->git_excludes_sha1);
+	hashcpy(fs_cache->user_excludes_sha1, hdr->user_excludes_sha1);
+
+	mmap_cur = hdr->strings;
+	mmap_cur = read_string(&fs_cache->last_update, mmap_cur);
+	mmap_cur = read_string(&fs_cache->repo_path, mmap_cur);
+	mmap_cur = read_string(&fs_cache->excludes_file, mmap_cur);
+
+	struct fsc_entry *parent_stack[PATH_MAX];
+	int parent_top = -1;
+
+	for (i = 0; i < nr; i++) {
+		struct ondisk_fsc_entry *disk_fe;
+		struct fsc_entry *fe;
+		unsigned long consumed;
+
+		disk_fe = (struct ondisk_fsc_entry *) mmap_cur;
+		fe = create_from_disk(fs_cache, disk_fe, &consumed);
+		/*
+		 * We eliminate deleted cache entries on read because
+		 * otherwise we have to count them in advance to fill
+		 * in nr, and that would be expensive.
+		 */
+		if (!fe_deleted(fe)) {
+			fs_cache_insert(fs_cache, fe);
+			if (parent_top == -1) {
+				parent_top = 0;
+				parent_stack[0] = fe;
+			} else {
+				char *p = parent_stack[parent_top]->path;
+				char *c = fe->path;
+				parent_top = 1;
+				for (; *p && *c; ++p, ++c) {
+					if (*p != *c)
+						break;
+					if (*p == '/')
+						parent_top ++;
+				}
+				if (*p == 0 && *c == '/')
+					parent_top ++;
+				parent_stack[parent_top] = fe;
+				fe_set_parent(fe, parent_stack[parent_top - 1]);
+			}
+		}
+		mmap_cur += consumed;
+	}
+
+	fs_cache->fully_loaded = 1;
+
+	munmap(mmap, mmap_size);
+
+	atexit(write_fs_cache_if_necessary);
+	return fs_cache;
+
+unmap:
+	munmap(mmap, mmap_size);
+	return NULL;
+}
+
+struct fs_cache *empty_fs_cache(void)
+{
+	struct fs_cache *fs_cache = xcalloc(1, sizeof(*fs_cache));
+	fs_cache->version = 1;
+	fs_cache->needs_write = 1;
+	fs_cache->fully_loaded = 1;
+	hashmap_init(&fs_cache->paths, (hashmap_cmp_fn) fe_entry_cmp, 1);
+	atexit(write_fs_cache_if_necessary);
+	return fs_cache;
+}
+
+struct fsc_entry *fs_cache_file_exists(const struct fs_cache *fs_cache,
+				       const char *name, int namelen)
+{
+	return fs_cache_file_exists_prehash(fs_cache, name, namelen,
+					  memihash(name, namelen));
+}
+
+struct fsc_entry *fs_cache_file_exists_prehash(const struct fs_cache *fs_cache, const char *path, int pathlen, unsigned int hash)
+{
+	struct fsc_entry key;
+
+	hashmap_entry_init(&key, hash);
+	key.pathlen = pathlen;
+	return hashmap_get(&fs_cache->paths, &key, path);
+}
+
+struct fsc_entry *make_fs_cache_entry(const char *path)
+{
+	return make_fs_cache_entry_len(path, strlen(path));
+}
+
+struct fsc_entry *make_fs_cache_entry_len(const char *path, int len)
+{
+	struct fsc_entry *fe = xcalloc(1, sizeof(*fe) + len + 1);
+	fe_set_new(fe);
+	memcpy(fe->path, path, len);
+	fe->pathlen = len;
+	hashmap_entry_init(fe, memihash(fe->path, fe->pathlen));
+	return fe;
+}
+
+void fs_cache_insert(struct fs_cache *fs_cache, struct fsc_entry *fe)
+{
+	hashmap_add(&fs_cache->paths, fe);
+	fs_cache->nr ++;
+}
+
+static void fs_cache_remove_recursive(struct fs_cache *fs_cache,
+				    struct fsc_entry *fe)
+{
+	struct fsc_entry *cur, *next;
+	for (cur = fe->first_child; cur; cur = next) {
+		fs_cache_remove_recursive(fs_cache, cur);
+		next = cur->next_sibling;
+		cur->next_sibling = NULL;
+		cur->parent = NULL;
+		cur->first_child = NULL;
+	}
+
+	hashmap_remove(&fs_cache->paths, fe, fe);
+	fs_cache->nr --;
+}
+
+static void fe_remove_from_parent(struct fsc_entry *fe)
+{
+	struct fsc_entry *prev, *cur;
+	if (fe->parent) {
+		prev = NULL;
+		for (cur = fe->parent->first_child; cur; cur = cur->next_sibling) {
+			if (cur == fe) {
+				if (prev)
+					prev->next_sibling = fe->next_sibling;
+				else
+					fe->parent->first_child = fe->next_sibling;
+				break;
+			}
+			prev = cur;
+		}
+	}
+}
+
+void fe_delete_children(struct fsc_entry *fe)
+{
+	for (fe = fe->first_child; fe; fe = fe->next_sibling) {
+		fe_set_deleted(fe);
+	}
+}
+
+void fe_clear_children(struct fs_cache *fs_cache, struct fsc_entry *fe) {
+	for (fe = fe->first_child; fe; fe = fe->next_sibling) {
+		fs_cache_remove(fs_cache, fe);
+	}
+
+}
+
+void fe_set_deleted(struct fsc_entry *fe)
+{
+	fe->flags |= FE_DELETED;
+	fe_delete_children(fe);
+}
+
+void fs_cache_remove_nonrecursive(struct fs_cache *fs_cache,
+				  struct fsc_entry *fe)
+{
+
+	hashmap_remove(&fs_cache->paths, fe, fe);
+	fs_cache->nr --;
+
+	fe_remove_from_parent(fe);
+}
+
+void fs_cache_remove(struct fs_cache *fs_cache,
+		   struct fsc_entry *fe)
+{
+
+	fs_cache_remove_recursive(fs_cache, fe);
+
+	fe_remove_from_parent(fe);
+}
+
+void free_fs_cache(struct fs_cache *fs_cache)
+{
+	obstack_free(&fs_cache->obs, NULL);
+	free(fs_cache->last_update);
+	free(fs_cache->repo_path);
+	free(fs_cache->excludes_file);
+}
+
+void fe_to_stat(struct fsc_entry *fe, struct stat *st)
+{
+	st->st_mtime = fe->mtime.sec;
+	st->st_ctime = fe->ctime.sec;
+#ifndef NO_NSEC
+#ifdef USE_ST_TIMESPEC
+	st->st_mtimespec.tv_nsec = fe->mtime.nsec;
+	st->st_ctimespec.tv_nsec = fe->ctime.nsec;
+#else
+	st->st_mtim.tv_nsec = fe->mtime.nsec;
+	st->st_ctim.tv_nsec = fe->ctime.nsec;
+#endif
+#endif
+	st->st_mode  = fe->mode;
+	st->st_ino  = fe->ino;
+	st->st_dev  = fe->dev;
+	st->st_uid  = fe->uid;
+	st->st_gid  = fe->gid;
+	st->st_size  = fe->size;
+}
+
+int is_in_dot_git(const char *name)
+{
+	char *evil = ".git";
+	char *cur = evil;
+	while (*name) {
+		if (*name == *cur++) {
+			name++;
+			if (*cur == 0) {
+				if (*name == 0 || *name == '/') {
+					return 1;
+				}
+			}
+		} else {
+			if (*name == '/') {
+				cur = evil;
+			} else {
+				cur = "";
+			}
+			name++;
+		}
+	}
+	return 0;
+}
+
+static int is_path_prefix(const char *putative_parent, const char *fname) {
+	const char* c;
+	for (c = putative_parent; *c && *fname; ++c, ++fname) {
+		if (*c != *fname) {
+			return 0;
+		}
+	}
+	return *c == 0 && (*fname == 0 || *fname == '/');
+}
+
+int fs_cache_open(struct fs_cache *fs_cache, const char *fname, int flags) {
+	if (fs_cache && fname[0] != '/' && !is_path_prefix(get_git_dir(), fname)) {
+		struct fsc_entry *fe = fs_cache_file_exists(fs_cache, fname, strlen(fname));
+		if (!fe || fe_deleted(fe)) {
+			errno = ENOENT;
+			return -1;
+		}
+	}
+	return open(fname, flags);
+}
+
+static const int topological_rank[256] = {
+	0, /* slash moved here */ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+	15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+	32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+	1 /* slash is special */,
+	48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
+	63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
+	79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94,
+	95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
+	107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
+	121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
+	135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148,
+	149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162,
+	163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
+	177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190,
+	191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204,
+	205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218,
+	219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232,
+	233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246,
+	247, 248, 249, 250, 251, 252, 253, 254, 255
+};
+
+/*
+ * Compare fsc_entry structs topologically -- that is, so that parent
+ * directories come before their children.
+ */
+int cmp_fsc_entry(const void *a, const void *b)
+{
+	struct fsc_entry* const * sa = a;
+	struct fsc_entry* const * sb = b;
+	const unsigned char* pa = (unsigned char *)(*sa)->path;
+	const unsigned char* pb = (unsigned char *)(*sb)->path;
+	while (*pa && *pb) {
+		int ca = topological_rank[*pa++];
+		int cb = topological_rank[*pb++];
+		int diff = ca - cb;
+		if (diff)
+			return diff;
+	}
+	return topological_rank[*pa] - topological_rank[*pb];
+}
diff --git a/fs_cache.h b/fs_cache.h
new file mode 100644
index 0000000..fb558e1
--- /dev/null
+++ b/fs_cache.h
@@ -0,0 +1,138 @@
+#ifndef FS_CACHE_H
+#define FS_CACHE_H
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "compat/obstack.h"
+
+#include "git-compat-util.h"
+#include "strbuf.h"
+#include "hashmap.h"
+
+#define obstack_chunk_alloc xmalloc
+#define obstack_chunk_free free
+
+/* The filesystem cache (fs_cache) stores the state of every file
+ * inside the root directory (excluding those in .git).  The state
+ * includes whether or not the file exists, as well as most of what
+ * lstat returns.
+ */
+
+#define fe_is_reg(fe) (S_ISREG((fe)->mode))
+#define fe_is_dir(fe) (S_ISDIR((fe)->mode))
+#define fe_is_lnk(fe) (S_ISLNK((fe)->mode))
+
+/* Directories get very different treatment generally; the normal bits
+ * don't apply to them, since they have no independent existence in
+ * git.  Also, they are subject to spooky action at a distance -- if a
+ * file called x/a/b/c is created (and added to the index), then x
+ * suddenly must get added to the index.
+ */
+
+struct fsc_entry {
+	struct hashmap_entry ent;
+	unsigned int mode;
+	off_t size;
+	unsigned int flags;
+	struct cache_time ctime;
+	struct cache_time mtime;
+	ino_t ino;
+	dev_t dev;
+	uid_t uid;
+	gid_t gid;
+	struct fsc_entry *parent;
+	struct fsc_entry *first_child;
+	struct fsc_entry *next_sibling;
+	int pathlen;
+	char path[FLEX_ARRAY];
+};
+
+#define FE_DELETED           (1 << 0)
+
+/* Excluded by the standard set of gitexcludes */
+#define FE_EXCLUDED          (1 << 8)
+
+/* Not yet saved to disk */
+#define FE_NEW               (1 << 10)
+
+void fe_set_deleted(struct fsc_entry *fe);
+#define fe_clear_deleted(fe) ((fe)->flags &= ~FE_DELETED)
+#define fe_deleted(fe) ((fe)->flags & FE_DELETED)
+
+#define fe_excluded(fe) ((fe)->flags & FE_EXCLUDED)
+#define fe_set_excluded(fe) ((fe)->flags |= FE_EXCLUDED)
+#define fe_clear_excluded(fe) ((fe)->flags &= ~FE_EXCLUDED)
+
+#define fe_new(fe) ((fe)->flags & FE_NEW)
+#define fe_set_new(fe) ((fe)->flags |= FE_NEW)
+#define fe_clear_new(fe) ((fe)->flags &= ~FE_NEW)
+
+struct fs_cache {
+	char *last_update;
+	char *repo_path;
+	char *excludes_file;
+	unsigned char git_excludes_sha1[20]; /* for .git/info/exclude */
+	unsigned char user_excludes_sha1[20]; /* for core.excludesfile */
+	unsigned int version;
+	struct hashmap paths;
+	int nr;
+	unsigned invalid : 1; /* A commit hook might have made fs
+			       * changes, necessitating a reload. */
+	unsigned needs_write : 1;
+	unsigned fully_loaded : 1;
+	uint32_t flags;
+	struct obstack obs;
+};
+
+struct fs_cache_header {
+	uint32_t hdr_signature;
+	uint32_t hdr_version;
+	uint32_t hdr_entries;
+	uint32_t flags;
+	unsigned char git_excludes_sha1[20];
+	unsigned char user_excludes_sha1[20];
+	char strings[FLEX_ARRAY];
+};
+
+struct ondisk_fsc_entry {
+	uint64_t ino;
+	uint64_t dev;
+	struct cache_time ctime;
+	struct cache_time mtime;
+	uint32_t mode;
+	uint32_t size;
+	uint32_t flags;
+	uint32_t uid;
+	uint32_t gid;
+	char path[FLEX_ARRAY];
+};
+
+extern char *get_fs_cache_file(void);
+
+unsigned char fe_dtype(struct fsc_entry *file);
+void fe_to_stat(struct fsc_entry *fe, struct stat *st);
+void fe_delete_children(struct fsc_entry *fe);
+void fe_clear_children(struct fs_cache *fs_cache, struct fsc_entry *fe);
+
+struct fs_cache *read_fs_cache(void);
+int write_fs_cache(struct fs_cache *fs_cache);
+struct fs_cache *empty_fs_cache(void);
+struct fsc_entry *fs_cache_file_exists(const struct fs_cache *fs_cache, const char *name, int namelen);
+struct fsc_entry *fs_cache_file_exists_prehash(const struct fs_cache *fs_cache, const char *name, int namelen, unsigned int hash);
+struct fsc_entry *make_fs_cache_entry(const char *path);
+struct fsc_entry *make_fs_cache_entry_len(const char *path, int len);
+void fs_cache_preload_metadata(char **last_update, char **repo_path);
+
+void fs_cache_remove(struct fs_cache *fs_cache, struct fsc_entry *fe);
+void fs_cache_insert(struct fs_cache *fs_cache, struct fsc_entry *fe);
+void free_fs_cache(struct fs_cache *fs_cache);
+void set_up_parent(struct fs_cache *fs_cache, struct fsc_entry *fe);
+
+int fs_cache_open(struct fs_cache *fs_cache, const char *fname, int flags);
+
+int is_in_dot_git(const char *name);
+
+int cmp_fsc_entry(const void *a, const void *b);
+
+#endif /* FS_CACHE_H */
diff --git a/hash-io.c b/hash-io.c
index ab4c7e4..907a360 100644
--- a/hash-io.c
+++ b/hash-io.c
@@ -154,11 +154,11 @@ void hash_context_init(struct hash_context *ctx, enum hash_io_type type)
 	ctx->write_buffer_len = 0;
 	switch (type) {
 	case HASH_IO_VMAC:
-		ctx->c.vc = malloc(sizeof *ctx->c.vc);
+		ctx->c.vc = xmalloc(sizeof *ctx->c.vc);
 		vmac_set_key(VMAC_KEY, ctx->c.vc);
 		break;
 	case HASH_IO_SHA1:
-		ctx->c.sc = malloc(sizeof *ctx->c.sc);
+		ctx->c.sc = xmalloc(sizeof *ctx->c.sc);
 		git_SHA1_Init(ctx->c.sc);
 		break;
 	default:
diff --git a/read-cache.c b/read-cache.c
index a124e59..5b1ca78 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -5,6 +5,7 @@
  */
 #define NO_THE_INDEX_COMPATIBILITY_MACROS
 #include "cache.h"
+#include "fs_cache.h"
 #include "cache-tree.h"
 #include "refs.h"
 #include "dir.h"
@@ -16,6 +17,10 @@
 #include "varint.h"
 #include "hash-io.h"
 
+#ifdef USE_WATCHMAN
+#include "watchman-support.h"
+#endif
+
 static struct cache_entry *refresh_cache_entry(struct cache_entry *ce,
 					       unsigned int options);
 
@@ -1004,6 +1009,30 @@ int add_index_entry(struct index_state *istate, struct cache_entry *ce, int opti
 	return 0;
 }
 
+static int fs_cache_lstat(struct fs_cache *fs_cache,
+			const char *name, int len, struct stat *st)
+{
+
+	struct fsc_entry *fe;
+	if (!fs_cache)
+		return lstat(name, st);
+
+	fe = fs_cache_file_exists(fs_cache, name, len);
+	if (!fe) {
+		/* This is necessary because children of symlinks are not
+		 * included in the fs_cache. */
+		return lstat(name, st);
+	}
+
+	if (fe_deleted(fe)) {
+		errno = ENOENT;
+		return -1;
+	} else {
+		fe_to_stat(fe, st);
+	}
+	return 0;
+}
+
 /*
  * "refresh" does not calculate a new sha1 file or bring the
  * cache up-to-date for mode/content changes. But what it
@@ -1045,7 +1074,7 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate,
 		return ce;
 	}
 
-	if (lstat(ce->name, &st) < 0) {
+	if (fs_cache_lstat(the_index.fs_cache, ce->name, ce_namelen(ce), &st) < 0) {
 		if (ignore_missing && errno == ENOENT)
 			return ce;
 		if (err)
@@ -1446,6 +1475,25 @@ static struct cache_entry *create_from_disk(struct ondisk_cache_entry *ondisk,
 	return ce;
 }
 
+static void do_load_fs_cache(struct index_state *istate, int force)
+{
+#ifdef USE_WATCHMAN
+	if (core_use_watchman && (istate->initialized || force)) {
+		if (istate->fs_cache) {
+			if (istate->fs_cache->invalid)
+				watchman_reload_fs_cache(istate);
+		} else {
+			if (watchman_load_fs_cache(istate)) {
+				if (istate->fs_cache) {
+					istate->fs_cache->needs_write = 0;
+					istate->fs_cache = NULL;
+				}
+			}
+		}
+	}
+#endif
+}
+
 /* remember to discard_cache() before reading a different cache! */
 int read_index_from(struct index_state *istate, const char *path)
 {
@@ -1457,6 +1505,8 @@ int read_index_from(struct index_state *istate, const char *path)
 	size_t mmap_size;
 	struct strbuf previous_name_buf = STRBUF_INIT, *previous_name;
 
+	do_load_fs_cache(istate, 0);
+
 	if (istate->initialized)
 		return istate->cache_nr;
 
@@ -1464,8 +1514,10 @@ int read_index_from(struct index_state *istate, const char *path)
 	istate->timestamp.nsec = 0;
 	fd = open(path, O_RDONLY);
 	if (fd < 0) {
-		if (errno == ENOENT)
+		if (errno == ENOENT) {
+			do_load_fs_cache(istate, 1);
 			return 0;
+		}
 		die_errno("index file open failed");
 	}
 
@@ -1530,6 +1582,9 @@ int read_index_from(struct index_state *istate, const char *path)
 		src_offset += 8;
 		src_offset += extsize;
 	}
+
+	do_load_fs_cache(istate, 0);
+
 	munmap(mmap, mmap_size);
 	return istate->cache_nr;
 
@@ -1560,6 +1615,8 @@ int discard_index(struct index_state *istate)
 	free(istate->cache);
 	istate->cache = NULL;
 	istate->cache_alloc = 0;
+	if (istate->fs_cache)
+		istate->fs_cache->invalid = 1;
 	return 0;
 }
 
diff --git a/t/t1012-read-tree-df.sh b/t/t1012-read-tree-df.sh
index a6a04b6..a677053 100755
--- a/t/t1012-read-tree-df.sh
+++ b/t/t1012-read-tree-df.sh
@@ -23,7 +23,7 @@ maketree () {
 }
 
 settree () {
-	rm -f .git/index .git/index.lock &&
+	rm -f .git/index .git/index.lock .git/fs_cache &&
 	git clean -d -f -f -q -x &&
 	git read-tree "$1" &&
 	git checkout-index -f -q -u -a &&
diff --git a/t/t2201-add-update-typechange.sh b/t/t2201-add-update-typechange.sh
index 954fc51..ad49535 100755
--- a/t/t2201-add-update-typechange.sh
+++ b/t/t2201-add-update-typechange.sh
@@ -124,7 +124,9 @@ test_expect_success diff-index '
 
 test_expect_success 'add -u' '
 	rm -f ".git/saved-index" &&
+	rm -f ".git/saved-fs_cache" &&
 	cp -p ".git/index" ".git/saved-index" &&
+	(test ! -f .git/fs_cache || cp -p ".git/fs_cache" ".git/saved-fs_cache") &&
 	git add -u &&
 	git ls-files -s >actual &&
 	test_cmp expect-final actual
@@ -134,7 +136,8 @@ test_expect_success 'commit -a' '
 	if test -f ".git/saved-index"
 	then
 		rm -f ".git/index" &&
-		mv ".git/saved-index" ".git/index"
+		mv ".git/saved-index" ".git/index" &&
+		(test ! -f .git/saved-fs_cache || mv ".git/saved-fs_cache" ".git/fs_cache")
 	fi &&
 	git commit -m "second" -a &&
 	git ls-files -s >actual &&
diff --git a/t/t2204-add-ignored.sh b/t/t2204-add-ignored.sh
index 8340ac2..6653aa8 100755
--- a/t/t2204-add-ignored.sh
+++ b/t/t2204-add-ignored.sh
@@ -18,7 +18,7 @@ test_expect_success setup '
 for i in file dir/file dir 'd*'
 do
 	test_expect_success "no complaints for unignored $i" '
-		rm -f .git/index &&
+		rm -f .git/index .git/fs_cache &&
 		git add "$i" &&
 		git ls-files "$i" >out &&
 		test -s out
@@ -28,7 +28,7 @@ done
 for i in ign dir/ign dir/sub dir/sub/*ign sub/file sub sub/*
 do
 	test_expect_success "complaints for ignored $i" '
-		rm -f .git/index &&
+		rm -f .git/index .git/fs_cache &&
 		test_must_fail git add "$i" 2>err &&
 		git ls-files "$i" >out &&
 		! test -s out
@@ -39,7 +39,7 @@ do
 	'
 
 	test_expect_success "complaints for ignored $i with unignored file" '
-		rm -f .git/index &&
+		rm -f .git/index .git/fs_cache &&
 		test_must_fail git add "$i" file 2>err &&
 		git ls-files "$i" >out &&
 		! test -s out
diff --git a/t/t6001-rev-list-graft.sh b/t/t6001-rev-list-graft.sh
index 8efcd13..56f8ac0 100755
--- a/t/t6001-rev-list-graft.sh
+++ b/t/t6001-rev-list-graft.sh
@@ -20,7 +20,7 @@ test_expect_success setup '
 	git commit -a -m "Third in one history." &&
 	A2=`git rev-parse --verify HEAD` &&
 
-	rm -f .git/refs/heads/master .git/index &&
+	rm -f .git/refs/heads/master .git/index .git/fs_cache &&
 
 	echo >fileA fileA again &&
 	echo >subdir/fileB fileB again &&
diff --git a/t/t7900-watchman.sh b/t/t7900-watchman.sh
new file mode 100755
index 0000000..bea6180
--- /dev/null
+++ b/t/t7900-watchman.sh
@@ -0,0 +1,249 @@
+#!/bin/sh
+#
+# Copyright (c) 2014 Twitter, Inc
+#
+
+test_description='Watchman'
+
+. ./test-lib.sh
+
+if ! test_have_prereq WATCHMAN
+then
+	skip_all='skipping watchman tests - no watchman'
+	test_done
+fi
+
+xpgrep () {
+	result=$(ps xopid,comm | grep " $1\$" | awk '{ print $1 }')
+	echo $result
+	test "x$result" != "x"
+}
+
+kill_watchman() {
+	#stop watchman
+	xpgrep watchman | xargs kill
+}
+
+#make sure that watchman is not running, but that it is runnable
+test_expect_success setup '
+	git config core.usewatchman true &&
+	#watchman is maybe running
+	xpgrep watchman > running-watchman
+	grep . running-watchman > /dev/null && kill $(cat running-watchman)
+	rm running-watchman &&
+	sleep 0.25 &&
+	#watchman is stopped
+	! xpgrep watchman > /dev/null &&
+	#watchman is startable
+	watchman &&
+	kill_watchman
+'
+
+cat >expect <<\EOF
+?? expect
+?? morx
+?? output
+EOF
+
+test_expect_success 'watchman sees new files' '
+	touch morx &&
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? expect
+?? output
+EOF
+
+test_expect_success 'watchman sees file deletion' '
+	rm morx &&
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+?? bramp
+EOF
+
+test_expect_success 'watchman understands .gitignore' '
+	touch bramp &&
+	cat >.gitignore <<-EOF &&
+	expect*
+	output*
+EOF
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+EOF
+
+test_expect_success 'watchman notices changes to .gitignore' '
+	cat >.gitignore <<-EOF &&
+	expect*
+	output*
+	bramp
+EOF
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+EOF
+
+test_expect_success 'watchman understands .git/info/exclude' '
+	touch fleem &&
+	cat >.git/info/exclude <<-EOF &&
+	fleem
+EOF
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+?? fleem
+EOF
+
+test_expect_success 'watchman notices changes to .git/info/exclude' '
+	touch crubbins &&
+	cat >.git/info/exclude <<-EOF &&
+	crubbins
+EOF
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+?? crubbins
+?? fleem
+EOF
+
+test_expect_success 'watchman notices removal of .git/info/exclude' '
+	rm .git/info/exclude &&
+	git status -s > output &&
+	test_cmp expect output &&
+	rm crubbins bramp fleem
+'
+
+
+cat >expect <<\EOF
+?? .gitignore
+?? fleem
+?? myignore
+EOF
+
+test_expect_success 'watchman notices changes to file configured by core.excludesfile' '
+	touch fleem &&
+	touch crubbins &&
+	cat >myignore <<-EOF &&
+	crubbins
+EOF
+	git config core.excludesfile myignore &&
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+?? crubbins
+?? myignore
+?? myignore2
+EOF
+
+test_expect_success 'watchman notices changes to config variable core.excludesfile' '
+	touch fleem &&
+	touch crubbins &&
+	cat >myignore2 <<-EOF &&
+	fleem
+EOF
+	git config core.excludesfile myignore2 &&
+	git status -s > output &&
+	test_cmp expect output
+'
+
+cat >expect <<\EOF
+?? .gitignore
+?? crubbins
+?? fleem
+?? myignore
+EOF
+
+test_expect_success 'watchman notices removal of file referred to by' '
+	rm myignore2 &&
+	git status -s > output &&
+	test_cmp expect output &&
+	rm crubbins fleem myignore
+'
+
+
+cat >expect.nothing <<\EOF
+EOF
+
+cat >expect.2 <<\EOF
+EOF
+
+test_expect_success 'git diff still works' '
+	echo 1 > diffy &&
+	git add diffy .gitignore &&
+	git commit -m initial &&
+	git status -s > output &&
+	test_cmp expect.nothing output &&
+	echo 2 >> diffy &&
+	test_cmp expect.2 output
+'
+
+cat >expect <<\EOF
+ D diffy
+EOF
+
+test_expect_success 'file to directory changes still work' '
+	rm diffy &&
+	mkdir diffy &&
+	touch diffy/a &&
+	git status -s > output &&
+	test_cmp expect output &&
+	git add diffy/a &&
+	git commit -m two &&
+	git status -s > output.nothing
+'
+
+cat >expect <<\EOF
+ D diffy/a
+?? diffy
+EOF
+
+test_expect_success 'directory to file changes still work' '
+	rm -r diffy &&
+	touch diffy &&
+	git status -s > output &&
+	test_cmp expect output &&
+	rm diffy &&
+	git rm diffy/a &&
+	git commit -m "remove diffy"
+'
+
+cat >expect <<\EOF
+?? dead
+EOF
+
+test_expect_success 'changes while watchman is not running are detected' '
+	kill_watchman &&
+	sleep 0.25 &&
+	! xpgrep watchman > /dev/null &&
+	touch dead &&
+	git status -s > output &&
+	test_cmp expect output
+'
+
+test_expect_success 'Restore default test environment' '
+	git config --unset core.usewatchman &&
+	kill_watchman
+'
+
+test_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index c081668..19d1ec5 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -781,6 +781,7 @@ test -z "$NO_PERL" && test_set_prereq PERL
 test -z "$NO_PYTHON" && test_set_prereq PYTHON
 test -n "$USE_LIBPCRE" && test_set_prereq LIBPCRE
 test -z "$NO_GETTEXT" && test_set_prereq GETTEXT
+test -n "$USE_WATCHMAN" && test_set_prereq WATCHMAN
 
 # Can we rely on git's output in the C locale?
 if test -n "$GETTEXT_POISON"
diff --git a/unpack-trees.c b/unpack-trees.c
index 97fc995..5161de3 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -1339,7 +1339,7 @@ static int verify_clean_subdirectory(const struct cache_entry *ce,
 	memset(&d, 0, sizeof(d));
 	if (o->dir)
 		d.exclude_per_dir = o->dir->exclude_per_dir;
-	i = read_directory(&d, pathbuf, namelen+1, NULL);
+	i = read_directory(o->src_index, &d, pathbuf, namelen+1, NULL);
 	if (i)
 		return o->gently ? -1 :
 			add_rejected_path(o, ERROR_NOT_UPTODATE_DIR, ce->name);
diff --git a/watchman-support.c b/watchman-support.c
new file mode 100644
index 0000000..9c5a87d
--- /dev/null
+++ b/watchman-support.c
@@ -0,0 +1,640 @@
+#include <dirent.h>
+
+#include "git-compat-util.h"
+#include "cache.h"
+#include "dir.h"
+#include "fs_cache.h"
+#include "strbuf.h"
+#include "pathspec.h"
+#include "watchman-support.h"
+
+#define NS_PER_SEC 1000000000L
+
+#define SET_TIME_FROM_NS(time, ns)	      \
+	do {				      \
+		(time).sec = (ns) / NS_PER_SEC;      \
+		(time).nsec = (ns) % NS_PER_SEC;     \
+	} while(0)
+
+static inline unsigned int create_fe_mode(unsigned int mode)
+{
+	if (S_ISLNK(mode))
+		return S_IFLNK;
+	if (S_ISDIR(mode))
+		return S_IFDIR;
+	return S_IFREG | ce_permissions(mode);
+}
+
+static void copy_wm_stat_to_fe(struct watchman_stat *wm, struct fsc_entry *fe)
+{
+	if (!wm->exists) {
+		fe_set_deleted(fe);
+		return;
+	} else
+		fe_clear_deleted(fe);
+	fe->size = wm->size;
+	fe->mode = create_fe_mode(wm->mode);
+	fe->ino = wm->ino;
+	fe->dev = wm->dev;
+	fe->uid = wm->uid;
+	fe->gid = wm->gid;
+	SET_TIME_FROM_NS(fe->mtime, wm->mtime_ns);
+	SET_TIME_FROM_NS(fe->ctime, wm->ctime_ns);
+	return;
+}
+
+static struct fsc_entry *wm_stat_to_fe(struct watchman_stat *wm)
+{
+	struct fsc_entry *fe = make_fs_cache_entry(wm->name);
+	fe_set_new(fe);
+	copy_wm_stat_to_fe(wm, fe);
+	return fe;
+}
+
+static void update_exclude(struct dir_struct *dir, struct fsc_entry *fe)
+{
+	int dtype = fe_dtype(fe);
+	if (is_excluded(dir, fe->path, &dtype)) {
+		fe_set_excluded(fe);
+	} else {
+		fe_clear_excluded(fe);
+	}
+	for (fe = fe->first_child; fe; fe = fe->next_sibling) {
+		update_exclude(dir, fe);
+	}
+}
+
+static struct fsc_entry *fs_cache_file_deleted(struct fs_cache *fs_cache,
+					       struct watchman_stat *wm)
+{
+	int namelen = strlen(wm->name);
+	struct fsc_entry *fe;
+
+	fe = fs_cache_file_exists(fs_cache, wm->name, namelen);
+
+	if (fe) {
+		fe_set_deleted(fe);
+		fe_clear_children(fs_cache, fe);
+	}
+
+	return fe;
+}
+
+static struct fsc_entry *fs_cache_file_modified(struct fs_cache *fs_cache,
+						struct watchman_stat *wm)
+{
+	int namelen = strlen(wm->name);
+	struct fsc_entry *fe;
+	fe = fs_cache_file_exists(fs_cache, wm->name, namelen);
+	if (!fe) {
+		fe = wm_stat_to_fe(wm);
+		fs_cache_insert(fs_cache, fe);
+		set_up_parent(fs_cache, fe);
+	} else {
+		int was_dir = fe_is_dir(fe);
+		if (fe_deleted(fe))
+			fe_set_new(fe);
+		copy_wm_stat_to_fe(wm, fe);
+		if (was_dir && !fe_is_dir(fe)) {
+			fe_clear_children(fs_cache, fe);
+		}
+	}
+	return fe;
+}
+
+static struct watchman_expression *make_expression()
+{
+	struct watchman_expression *types[3];
+	types[0] = watchman_type_expression('f');
+	types[1] = watchman_type_expression('d');
+	types[2] = watchman_type_expression('l');
+	struct watchman_expression *expr = watchman_anyof_expression(3, types);
+
+	return expr;
+}
+
+struct watchman_query *make_query(const char *last_update)
+{
+	struct watchman_query *query = watchman_query();
+
+	watchman_query_set_fields(query,
+				  WATCHMAN_FIELD_NAME |
+				  WATCHMAN_FIELD_MTIME_NS |
+				  WATCHMAN_FIELD_CTIME_NS |
+				  WATCHMAN_FIELD_INO |
+				  WATCHMAN_FIELD_DEV |
+				  WATCHMAN_FIELD_UID |
+				  WATCHMAN_FIELD_GID |
+				  WATCHMAN_FIELD_EXISTS |
+				  WATCHMAN_FIELD_MODE |
+				  WATCHMAN_FIELD_SIZE);
+	watchman_query_set_empty_on_fresh(query, 1);
+
+	query->sync_timeout = core_watchman_sync_timeout;
+
+	if (last_update) {
+		watchman_query_set_since_oclock(query, last_update);
+	}
+	return query;
+}
+
+enum path_treatment {
+	path_recurse,
+	path_file
+};
+
+void fe_from_stat(struct fsc_entry *fe, struct stat *st)
+{
+	fe->mode = create_fe_mode(st->st_mode);
+	fe->size = st->st_size;
+	fe->ino = st->st_ino;
+	fe->dev = st->st_dev;
+	fe->ctime.sec = st->st_ctime;
+	fe->ctime.nsec = ST_CTIME_NSEC(*st);
+	fe->mtime.sec = st->st_mtime;
+	fe->mtime.nsec = ST_MTIME_NSEC(*st);
+	fe->uid = st->st_uid;
+	fe->gid = st->st_gid;
+}
+
+static void update_all_excludes(struct fs_cache *fs_cache)
+{
+	struct fsc_entry *root = fs_cache_file_exists(fs_cache, "", 0);
+	struct dir_struct dir;
+	char original_path[PATH_MAX + 1];
+	const char *fs_path = get_git_work_tree();
+
+	if (!getcwd(original_path, PATH_MAX + 1))
+			die_errno("failed to get working directory\n");
+	if (chdir(fs_path))
+			die_errno("failed to chdir to git work tree\n");
+
+	assert (root);
+
+	memset(&dir, 0, sizeof(dir));
+	setup_standard_excludes(&dir);
+	update_exclude(&dir, root);
+	clear_directory(&dir);
+
+	if (chdir(original_path))
+			die_errno("failed to chdir back to original path\n");
+}
+
+static enum path_treatment watchman_handle(struct index_state *istate, struct strbuf *path, struct dirent *de, int rootlen, struct fsc_entry **out)
+{
+	struct fs_cache *fs_cache = istate->fs_cache;
+	struct fsc_entry *fe;
+	struct stat st;
+	int dtype;
+
+	fe = make_fs_cache_entry(path->buf + rootlen);
+	*out = fe;
+	fs_cache_insert(fs_cache, fe);
+	set_up_parent(fs_cache, fe);
+	lstat(path->buf, &st);
+	fe_from_stat(fe, &st);
+
+	dtype = DTYPE(de);
+	if (dtype == DT_UNKNOWN) {
+		/* this involves an extra stat call, but only on
+		 * Cygwin, which watchman doesn't support anyway. */
+		dtype = get_dtype(de, path->buf, path->len);
+	}
+	if (dtype == DT_DIR) {
+		return path_recurse;
+	}
+
+	return path_file;
+}
+
+static void path_set_last_component(struct strbuf *path, int baselen, const char *add)
+{
+	strbuf_setlen(path, baselen);
+	if (baselen) {
+		strbuf_addch(path, '/');
+	}
+	strbuf_addstr(path, add);
+}
+
+static int preload_wt_recursive(struct index_state *istate, struct strbuf *path, int rootlen)
+{
+	DIR *fdir;
+	struct dirent *de;
+	int baselen = path->len;
+
+	fdir = opendir(path->buf);
+	if (!fdir) {
+		return error("Failed to open %s", path->buf);
+	}
+
+	while ((de = readdir(fdir)) != NULL) {
+		struct fsc_entry *fe;
+		if (is_dot_or_dotdot(de->d_name) || is_in_dot_git(de->d_name))
+			continue;
+
+		path_set_last_component(path, baselen, de->d_name);
+
+		/* recurse into subdir if necessary */
+		if (watchman_handle(istate, path, de, rootlen, &fe) == path_recurse) {
+			int result = preload_wt_recursive(istate, path, rootlen);
+			if (result) {
+				closedir(fdir);
+				return result;
+			}
+		}
+	}
+
+	closedir(fdir);
+	return 0;
+}
+
+static void init_excludes_config()
+{
+	char *xdg_path;
+	if (!excludes_file) {
+		home_config_paths(NULL, &xdg_path, "ignore");
+		excludes_file = xdg_path;
+	}
+}
+
+static void compute_sha1(const char *path, unsigned char *sha1)
+{
+	struct stat st;
+	if (stat(path, &st)) {
+		memset(sha1, 0, 20);
+	} else {
+		if (index_path(sha1, path, &st, 0)) {
+			memset(sha1, 0, 20);
+		}
+	}
+}
+
+static void init_excludes_files(struct fs_cache *fs_cache)
+{
+	init_excludes_config();
+	if (fs_cache->excludes_file) {
+		free(fs_cache->excludes_file);
+	}
+	if (excludes_file) {
+		fs_cache->excludes_file = xstrdup(excludes_file);
+		compute_sha1(excludes_file, fs_cache->user_excludes_sha1);
+	} else {
+		fs_cache->excludes_file = xstrdup("");
+		memset(fs_cache->user_excludes_sha1, 0, 20);
+	}
+	compute_sha1(git_path("info/excludes"), fs_cache->git_excludes_sha1);
+}
+
+static int git_excludes_file_changed(struct fs_cache *fs_cache)
+{
+	unsigned char sha1[20];
+
+	compute_sha1(git_path("info/exclude"), sha1);
+	if (!hashcmp(fs_cache->git_excludes_sha1, sha1))
+		return 0;
+	hashcpy(fs_cache->git_excludes_sha1, sha1);
+	return 1;
+}
+
+static int user_excludes_file_changed(struct fs_cache *fs_cache)
+{
+	unsigned char sha1[20] = {0};
+	struct stat st;
+
+	init_excludes_config();
+
+	if (!excludes_file) {
+		if (strlen(fs_cache->excludes_file) == 0) {
+			return 0;
+		}
+
+		fs_cache->excludes_file[0] = 0;
+		if (is_null_sha1(fs_cache->user_excludes_sha1))
+			return 0;
+
+		memset(fs_cache->user_excludes_sha1, 0, 20);
+		return 1;
+	}
+
+	/* A change in exclude filename forces an exclude reload */
+	if (strcmp(fs_cache->excludes_file, excludes_file)) {
+		init_excludes_files(fs_cache);
+		return 1;
+	}
+
+	if (!strlen(fs_cache->excludes_file)) {
+		return 0;
+	}
+
+	if (stat(excludes_file, &st)) {
+		/* There is a problem reading the excludes file; this
+		 * could be a persistent condition, so we need to
+		 * check if the file is presently marked as invalid */
+		if (is_null_sha1(fs_cache->user_excludes_sha1))
+			return 0;
+		else {
+			memset(fs_cache->user_excludes_sha1, 0, 20);
+			return 1;
+		}
+	}
+
+	if (index_path(sha1, excludes_file, &st, 0)) {
+		if (is_null_sha1(fs_cache->user_excludes_sha1)) {
+			return 0;
+		} else {
+			memset(fs_cache->user_excludes_sha1, 0, 20);
+			return 1;
+		}
+	} else {
+		if (!hashcmp(fs_cache->user_excludes_sha1, sha1))
+			return 0;
+		hashcpy(fs_cache->user_excludes_sha1, sha1);
+		return 1;
+	}
+}
+
+static void create_fs_cache(struct index_state *istate)
+{
+	struct strbuf buf = STRBUF_INIT;
+	const char *fs_path = get_git_work_tree();
+	struct fsc_entry *root;
+
+	strbuf_addstr(&buf, fs_path);
+	istate->fs_cache = empty_fs_cache();
+	root = make_fs_cache_entry("");
+	root->mode = 040644;
+	fs_cache_insert(istate->fs_cache, root);
+	preload_wt_recursive(istate, &buf, buf.len + 1);
+	strbuf_release(&buf);
+
+	init_excludes_files(istate->fs_cache);
+	update_all_excludes(istate->fs_cache);
+}
+
+static void load_fs_cache(struct index_state *istate)
+{
+	if (istate->fs_cache)
+		return;
+	istate->fs_cache = read_fs_cache();
+	if (!istate->fs_cache) {
+		create_fs_cache(istate);
+	}
+}
+
+static struct watchman_query_result *watchman_fs_cache_query(struct watchman_connection *connection, const char *fs_path, const char *last_update)
+{
+	struct watchman_error wm_error;
+	struct watchman_expression *expr;
+	struct watchman_query *query;
+	struct watchman_query_result *result = NULL;
+	struct stat st;
+	int fs_path_len = strlen(fs_path);
+	char *git_path;
+
+	expr = make_expression();
+	query = make_query(last_update);
+	if (lstat(fs_path, &st)) {
+		return NULL;
+	}
+
+	git_path = xmalloc(fs_path_len + 6);
+	strcpy(git_path, fs_path);
+	strcpy(git_path + fs_path_len, "/.git");
+
+	if (lstat(git_path, &st)) {
+		/* Watchman gets confused if we delete the .git
+		 * directory out from under it, since that's where it
+		 * stores its cookies.  So we'll need to delete the
+		 * watch and then recreate it. It's OK for this to
+		 * fail, as the watch might have already been
+		 * deleted. */
+		watchman_watch_del(connection, fs_path, &wm_error);
+
+		if (watchman_watch(connection, fs_path, &wm_error)) {
+			warning("Watchman watch error: %s", wm_error.message);
+			goto out;
+		}
+	}
+	result = watchman_do_query(connection, fs_path, query, expr, &wm_error);
+	if (!result) {
+		warning("Watchman query error: %s (at %s)", wm_error.message, last_update);
+		goto out;
+	}
+	watchman_free_expression(expr);
+	watchman_free_query(query);
+
+out:
+	free(git_path);
+	return result;
+}
+
+static int cmp_stat(const void *a, const void *b)
+{
+	const struct watchman_stat* sa = a;
+	const struct watchman_stat* sb = b;
+	return strcmp(sa->name, sb->name);
+}
+
+static void append(struct fsc_entry ***list, int* cap, int* len, struct fsc_entry *entry)
+{
+	if (*len >= *cap) {
+		int sz;
+		*cap = *cap ? *cap * 2 : 10;
+		sz = *cap * sizeof(**list);
+		*list = xrealloc(*list, sz);
+	}
+	(*list)[(*len)++] = entry;
+}
+
+static int is_child_of(struct fsc_entry *putative_child, struct fsc_entry *parent)
+{
+	while (putative_child) {
+		putative_child = putative_child->parent;
+		if (putative_child == parent) {
+			return 1;
+		}
+	}
+	return 0;
+}
+
+static void update_fs_cache(struct index_state *istate, struct watchman_query_result *result)
+{
+	struct fs_cache *fs_cache = istate->fs_cache;
+	struct fsc_entry *fe;
+	int i;
+	struct fsc_entry **exclude_dirty = NULL;
+	int cap = 0, len = 0, all_dirty = 0;
+	/* note that we always want to call both of these functions,
+	 * since they update the fs_cache's view of files which are
+	 * not watched by watchman */
+	int user_changed = user_excludes_file_changed(fs_cache);
+	int git_changed = git_excludes_file_changed(fs_cache);
+
+	all_dirty = user_changed || git_changed;
+
+	qsort(result->stats, result->nr, sizeof(*result->stats), cmp_stat);
+
+	for (i = 0; i < result->nr; ++i) {
+		/*for each result in the changed set, we need to check
+		  it against the index and HEAD */
+
+		struct watchman_stat *wm = result->stats + i;
+
+		if (is_in_dot_git(wm->name)) {
+			continue;
+		}
+		fs_cache->needs_write = 1;
+		if (wm->exists) {
+			fe = fs_cache_file_modified(fs_cache, wm);
+		} else {
+			fe = fs_cache_file_deleted(fs_cache, wm);
+		}
+		if (fe && !all_dirty) {
+			if (ends_with(wm->name, "/.gitignore") ||
+			    !strcmp(wm->name, ".gitignore")) {
+				append(&exclude_dirty, &cap, &len, fe->parent);
+			} else if (fe_new(fe)) {
+				append(&exclude_dirty, &cap, &len, fe);
+			}
+		}
+	}
+
+	if (exclude_dirty) {
+		struct dir_struct dir;
+		struct fsc_entry *last = NULL;
+		char original_path[PATH_MAX + 1];
+		qsort(exclude_dirty, len, sizeof(*exclude_dirty), cmp_fsc_entry);
+
+		if (!getcwd(original_path, PATH_MAX + 1))
+			die_errno("failed to get working directory\n");
+		if (chdir(get_git_work_tree()))
+			die_errno("failed to chdir to git work tree\n");
+
+		memset(&dir, 0, sizeof(dir));
+		setup_standard_excludes(&dir);
+
+		for (i = 0; i < len; i++) {
+			struct fsc_entry *fe = exclude_dirty[i];
+
+			if (i == 0 || !is_child_of(fe, last)) {
+				update_exclude(&dir, fe);
+				last = fe;
+			}
+		}
+		clear_directory(&dir);
+		free(exclude_dirty);
+		if (chdir(original_path))
+			die_errno("failed to chdir back to original path\n");
+	} else if (all_dirty) {
+		update_all_excludes(fs_cache);
+	}
+
+}
+
+int watchman_reload_fs_cache(struct index_state *istate)
+{
+	struct watchman_error wm_error;
+	struct watchman_query_result *result;
+	struct watchman_connection *connection;
+	int ret = -1;
+	const char *fs_path;
+	const char *last_update = istate->fs_cache->last_update;
+
+	fs_path = get_git_work_tree();
+	if (!fs_path)
+		return -1;
+
+	connection = watchman_connect(&wm_error);
+
+	if (!connection) {
+		warning("Watchman watch error: %s", wm_error.message);
+		return -1;
+	}
+
+	result = watchman_fs_cache_query(connection, fs_path, last_update);
+	if (!result) {
+		goto done;
+	}
+	istate->fs_cache->last_update = xstrdup(result->clock);
+
+	update_fs_cache(istate, result);
+	watchman_free_query_result(result);
+	ret = 0;
+done:
+	watchman_connection_close(connection);
+	return ret;
+}
+
+int watchman_load_fs_cache(struct index_state *istate)
+{
+	struct watchman_error wm_error;
+	int ret = -1;
+	const char *fs_path;
+	char *last_update = NULL;
+	char *stored_repo_path = NULL;
+	struct watchman_query_result *result;
+	struct watchman_connection *connection;
+
+	fs_path = get_git_work_tree();
+	if (!fs_path)
+		return -1;
+
+	connection = watchman_connect(&wm_error);
+
+	if (!connection) {
+		warning("Watchman watch error: %s", wm_error.message);
+		return -1;
+	}
+
+	if (watchman_watch(connection, fs_path, &wm_error)) {
+		warning("Watchman watch error: %s", wm_error.message);
+		goto done;
+	}
+
+	fs_cache_preload_metadata(&last_update, &stored_repo_path);
+	if (!last_update || strcmp(stored_repo_path, fs_path)) {
+		if (istate->fs_cache) {
+			free_fs_cache(istate->fs_cache);
+			istate->fs_cache = NULL;
+		}
+		/* fs_cache is corrupt, or refers to another repo path;
+		 * let's try recreating it. */
+		if (last_update)
+			free(last_update);
+		last_update = NULL;
+		/* now we continue, because we need to get the
+		 * a last-update time from watchman. */
+	}
+	free(stored_repo_path);
+
+	result = watchman_fs_cache_query(connection, fs_path, last_update);
+	if (last_update) {
+		free(last_update);
+		last_update = NULL;
+	}
+	if (!result) {
+		goto done;
+	}
+
+	if (result->is_fresh_instance) {
+		if (istate->fs_cache) {
+			free_fs_cache(istate->fs_cache);
+			istate->fs_cache = NULL;
+		}
+		create_fs_cache(istate);
+		istate->fs_cache->repo_path = xstrdup(fs_path);
+	} else {
+		load_fs_cache(istate);
+		update_fs_cache(istate, result);
+	}
+
+	istate->fs_cache->last_update = xstrdup(result->clock);
+
+	watchman_free_query_result(result);
+	ret = 0;
+
+done:
+	watchman_connection_close(connection);
+	return ret;
+
+}
diff --git a/watchman-support.h b/watchman-support.h
new file mode 100644
index 0000000..1ab865f
--- /dev/null
+++ b/watchman-support.h
@@ -0,0 +1,10 @@
+#ifndef WATCHMAN_SUPPORT_H
+#define WATCHMAN_SUPPORT_H
+
+#include "cache.h"
+#include <watchman.h>
+
+int watchman_load_fs_cache(struct index_state *index);
+int watchman_reload_fs_cache(struct index_state *index);
+
+#endif /* WATCHMAN_SUPPORT_H */
-- 
2.0.0.rc0.31.g69c1a2d

  parent reply	other threads:[~2014-05-02 23:15 UTC|newest]

Thread overview: 43+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2014-05-02 23:14 Watchman support for git dturner
2014-05-02 23:14 ` [PATCH 1/3] After chdir to run grep, return to old directory dturner
2014-05-06 22:24   ` Junio C Hamano
2014-05-07  0:06     ` David Turner
2014-05-07  3:00       ` Jeff King
2014-05-07  3:33         ` David Turner
2014-05-07 17:42           ` Junio C Hamano
2014-05-07 20:57             ` David Turner
2014-05-02 23:14 ` dturner [this message]
2014-05-02 23:20 ` Watchman support for git Felipe Contreras
2014-05-03  2:24   ` David Turner
2014-05-03  3:40     ` Felipe Contreras
2014-05-05 18:08       ` David Turner
2014-05-05 18:14         ` Felipe Contreras
2014-05-08 19:17       ` Sebastian Schuberth
2014-05-09  7:08         ` David Lang
2014-05-09 17:17           ` David Turner
2014-05-09 18:08             ` David Lang
2014-05-09 18:17               ` David Turner
2014-05-09 18:27                 ` David Lang
2014-05-09 18:47                   ` David Turner
2014-05-03  0:52 ` Duy Nguyen
2014-05-03  4:39   ` David Turner
2014-05-03  8:49     ` Duy Nguyen
2014-05-03 20:49       ` David Turner
2014-05-04  0:15         ` Duy Nguyen
2014-05-06  3:13           ` David Turner
2014-05-06  0:26   ` Duy Nguyen
2014-05-06  0:30     ` Duy Nguyen
2014-05-10  5:26 ` Duy Nguyen
2014-05-10 18:38   ` David Turner
2014-05-11  0:21     ` Duy Nguyen
2014-05-11 22:56       ` David Turner
2014-05-12 10:45         ` Duy Nguyen
2014-05-13 22:38           ` David Turner
2014-05-13 22:54             ` Duy Nguyen
2014-05-13 23:19               ` David Turner
2014-05-10  8:16 ` Duy Nguyen
2014-05-13 23:44   ` David Turner
2014-05-14 10:36     ` Duy Nguyen
2014-05-14 10:52       ` Duy Nguyen
2014-05-15 19:42       ` David Turner
2014-05-19 10:10         ` Duy Nguyen

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1399072451-15561-4-git-send-email-dturner@twopensource.com \
    --to=dturner@twopensource.com \
    --cc=dturner@twitter.com \
    --cc=git@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).