summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2017-07-05 15:12:17 -0700
committerJunio C Hamano <gitster@pobox.com>2017-07-05 15:12:17 -0700
commitd1f9377166b226f023574a6b44066b83e0b06cab (patch)
tree43225b01b71a16117c812796a9b2bfde93469490
parent3cfb922c005ddf2f0112a958d960fb8df3e32d4e (diff)
parent06fe0892d855943b0ecf76103cd0104e8a64782b (diff)
downloadgit-d1f9377166b226f023574a6b44066b83e0b06cab.tar.gz
Merge branch 'bp/fsmonitor' into pu
We learned to talk to watchman to speed up "git status". No more comments or updates? * bp/fsmonitor: fsmonitor: add a sample query-fsmonitor hook script for Watchman fsmonitor: add documentation for the fsmonitor extension. fsmonitor: add test cases for fsmonitor extension fsmonitor: teach git to optionally utilize a file system monitor to speed up detecting new or changed files. dir: make lookup_untracked() available outside of dir.c bswap: add 64 bit endianness helper get_be64
-rw-r--r--Documentation/config.txt7
-rw-r--r--Documentation/githooks.txt23
-rw-r--r--Documentation/technical/index-format.txt19
-rw-r--r--Makefile1
-rw-r--r--builtin/update-index.c1
-rw-r--r--cache.h5
-rw-r--r--compat/bswap.h4
-rw-r--r--config.c4
-rw-r--r--dir.c29
-rw-r--r--dir.h5
-rw-r--r--entry.c1
-rw-r--r--environment.c1
-rw-r--r--fsmonitor.c262
-rw-r--r--fsmonitor.h9
-rw-r--r--read-cache.c28
-rwxr-xr-xt/t7519-status-fsmonitor.sh173
-rwxr-xr-xtemplates/hooks--query-fsmonitor.sample76
-rw-r--r--unpack-trees.c1
18 files changed, 636 insertions, 13 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index c75872328e..a3c822793b 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -413,6 +413,13 @@ core.protectNTFS::
8.3 "short" names.
Defaults to `true` on Windows, and `false` elsewhere.
+core.fsmonitor::
+ If set to true, call the query-fsmonitor hook proc which will
+ identify all files that may have had changes since the last
+ request. This information is used to speed up operations like
+ 'git commit' and 'git status' by limiting what git must scan to
+ detect changes.
+
core.trustctime::
If false, the ctime differences between the index and the
working tree are ignored; useful when the inode change time
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b2514f4d44..219786b2da 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -456,6 +456,29 @@ non-zero status causes 'git send-email' to abort before sending any
e-mails.
+[[query-fsmonitor]]
+query-fsmonitor
+~~~~~~~~~~~~~~~
+
+This hook is invoked when the configuration option core.fsmonitor is
+set and git needs to identify changed or untracked files. It takes
+two arguments, a version (currently 1) and the time in elapsed
+nanoseconds since midnight, January 1, 1970.
+
+The hook should output to stdout the list of all files in the working
+directory that may have changed since the requested time. The logic
+should be inclusive so that it does not miss any potential changes.
+The paths should be relative to the root of the working directory
+and be separated by a single NUL.
+
+Git will limit what files it checks for changes as well as which
+directories are checked for untracked files based on the path names
+given.
+
+The exit status determines whether git will use the data from the
+hook to limit its search. On error, it will fall back to verifying
+all files and folders.
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/Documentation/technical/index-format.txt b/Documentation/technical/index-format.txt
index ade0b0c445..7aeeea6f94 100644
--- a/Documentation/technical/index-format.txt
+++ b/Documentation/technical/index-format.txt
@@ -295,3 +295,22 @@ The remaining data of each directory block is grouped by type:
in the previous ewah bitmap.
- One NUL.
+
+== File System Monitor cache
+
+ The file system monitor cache tracks files for which the query-fsmonitor
+ hook has told us about changes. The signature for this extension is
+ { 'F', 'S', 'M', 'N' }.
+
+ The extension starts with
+
+ - 32-bit version number: the current supported version is 1.
+
+ - 64-bit time: the extension data reflects all changes through the given
+ time which is stored as the nanoseconds elapsed since midnight,
+ January 1, 1970.
+
+ - 32-bit bitmap size: the size of the CE_FSMONITOR_DIRTY bitmap.
+
+ - An ewah bitmap, the n-th bit indicates whether the n-th index entry
+ is CE_FSMONITOR_DIRTY.
diff --git a/Makefile b/Makefile
index 9d50a5aada..46d34a79de 100644
--- a/Makefile
+++ b/Makefile
@@ -785,6 +785,7 @@ LIB_OBJS += ewah/ewah_rlw.o
LIB_OBJS += exec_cmd.o
LIB_OBJS += fetch-pack.o
LIB_OBJS += fsck.o
+LIB_OBJS += fsmonitor.o
LIB_OBJS += gettext.o
LIB_OBJS += gpg-interface.o
LIB_OBJS += graph.o
diff --git a/builtin/update-index.c b/builtin/update-index.c
index 56721cf03d..f272e160c1 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -233,6 +233,7 @@ static int mark_ce_flags(const char *path, int flag, int mark)
else
active_cache[pos]->ce_flags &= ~flag;
active_cache[pos]->ce_flags |= CE_UPDATE_IN_BASE;
+ active_cache[pos]->ce_flags |= CE_FSMONITOR_DIRTY;
cache_tree_invalidate_path(&the_index, path);
active_cache_changed |= CE_ENTRY_CHANGED;
return 0;
diff --git a/cache.h b/cache.h
index 6c8242340b..c49c140442 100644
--- a/cache.h
+++ b/cache.h
@@ -203,6 +203,7 @@ struct cache_entry {
#define CE_ADDED (1 << 19)
#define CE_HASHED (1 << 20)
+#define CE_FSMONITOR_DIRTY (1 << 21)
#define CE_WT_REMOVE (1 << 22) /* remove in work directory */
#define CE_CONFLICTED (1 << 23)
@@ -326,6 +327,7 @@ static inline unsigned int canon_mode(unsigned int mode)
#define CACHE_TREE_CHANGED (1 << 5)
#define SPLIT_INDEX_ORDERED (1 << 6)
#define UNTRACKED_CHANGED (1 << 7)
+#define FSMONITOR_CHANGED (1 << 8)
struct split_index;
struct untracked_cache;
@@ -344,6 +346,8 @@ struct index_state {
struct hashmap dir_hash;
unsigned char sha1[20];
struct untracked_cache *untracked;
+ uint64_t fsmonitor_last_update;
+ struct ewah_bitmap *fsmonitor_dirty;
};
extern struct index_state the_index;
@@ -773,6 +777,7 @@ extern int core_apply_sparse_checkout;
extern int precomposed_unicode;
extern int protect_hfs;
extern int protect_ntfs;
+extern int core_fsmonitor;
/*
* Include broken refs in all ref iterations, which will
diff --git a/compat/bswap.h b/compat/bswap.h
index d47c003544..f89fe7f4b5 100644
--- a/compat/bswap.h
+++ b/compat/bswap.h
@@ -158,6 +158,7 @@ static inline uint64_t git_bswap64(uint64_t x)
#define get_be16(p) ntohs(*(unsigned short *)(p))
#define get_be32(p) ntohl(*(unsigned int *)(p))
+#define get_be64(p) ntohll(*(uint64_t *)(p))
#define put_be32(p, v) do { *(unsigned int *)(p) = htonl(v); } while (0)
#else
@@ -170,6 +171,9 @@ static inline uint64_t git_bswap64(uint64_t x)
(*((unsigned char *)(p) + 1) << 16) | \
(*((unsigned char *)(p) + 2) << 8) | \
(*((unsigned char *)(p) + 3) << 0) )
+#define get_be64(p) ( \
+ ((uint64_t)get_be32((unsigned char *)(p) + 0) << 32) | \
+ ((uint64_t)get_be32((unsigned char *)(p) + 4) << 0)
#define put_be32(p, v) do { \
unsigned int __v = (v); \
*((unsigned char *)(p) + 0) = __v >> 24; \
diff --git a/config.c b/config.c
index a1b66fe764..f59c30de3a 100644
--- a/config.c
+++ b/config.c
@@ -1238,6 +1238,10 @@ static int git_default_core_config(const char *var, const char *value)
hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
else
hide_dotfiles = git_config_bool(var, value);
+ }
+
+ if (!strcmp(var, "core.fsmonitor")) {
+ core_fsmonitor = git_config_bool(var, value);
return 0;
}
diff --git a/dir.c b/dir.c
index ae6f5c9636..aa9f95df6d 100644
--- a/dir.c
+++ b/dir.c
@@ -18,6 +18,7 @@
#include "utf8.h"
#include "varint.h"
#include "ewah/ewok.h"
+#include "fsmonitor.h"
/*
* Tells read_directory_recursive how a file or directory should be treated.
@@ -674,7 +675,7 @@ static void trim_trailing_spaces(char *buf)
*
* If "name" has the trailing slash, it'll be excluded in the search.
*/
-static struct untracked_cache_dir *lookup_untracked(struct untracked_cache *uc,
+struct untracked_cache_dir *lookup_untracked(struct untracked_cache *uc,
struct untracked_cache_dir *dir,
const char *name, int len)
{
@@ -1688,17 +1689,23 @@ static int valid_cached_dir(struct dir_struct *dir,
if (!untracked)
return 0;
- if (stat(path->len ? path->buf : ".", &st)) {
- invalidate_directory(dir->untracked, untracked);
- memset(&untracked->stat_data, 0, sizeof(untracked->stat_data));
- return 0;
- }
- if (!untracked->valid ||
- match_stat_data_racy(istate, &untracked->stat_data, &st)) {
- if (untracked->valid)
+ /*
+ * With fsmonitor, we can trust the untracked cache's valid field.
+ */
+ refresh_by_fsmonitor(&the_index);
+ if (!(dir->untracked->use_fsmonitor && untracked->valid)) {
+ if (stat(path->len ? path->buf : ".", &st)) {
invalidate_directory(dir->untracked, untracked);
- fill_stat_data(&untracked->stat_data, &st);
- return 0;
+ memset(&untracked->stat_data, 0, sizeof(untracked->stat_data));
+ return 0;
+ }
+ if (!untracked->valid ||
+ match_stat_data_racy(istate, &untracked->stat_data, &st)) {
+ if (untracked->valid)
+ invalidate_directory(dir->untracked, untracked);
+ fill_stat_data(&untracked->stat_data, &st);
+ return 0;
+ }
}
if (untracked->check_only != !!check_only) {
diff --git a/dir.h b/dir.h
index e3717055d1..e3f463b714 100644
--- a/dir.h
+++ b/dir.h
@@ -139,6 +139,8 @@ struct untracked_cache {
int gitignore_invalidated;
int dir_invalidated;
int dir_opened;
+ /* fsmonitor invalidation data */
+ unsigned int use_fsmonitor : 1;
};
struct dir_struct {
@@ -357,4 +359,7 @@ extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git
extern void relocate_gitdir(const char *path,
const char *old_git_dir,
const char *new_git_dir);
+struct untracked_cache_dir *lookup_untracked(struct untracked_cache *uc,
+ struct untracked_cache_dir *dir,
+ const char *name, int len);
#endif
diff --git a/entry.c b/entry.c
index 65458f07a4..acfe01a0cf 100644
--- a/entry.c
+++ b/entry.c
@@ -344,6 +344,7 @@ finish:
lstat(ce->name, &st);
fill_stat_cache_info(ce, &st);
ce->ce_flags |= CE_UPDATE_IN_BASE;
+ ce->ce_flags |= CE_FSMONITOR_DIRTY;
state->istate->cache_changed |= CE_ENTRY_CHANGED;
}
return 0;
diff --git a/environment.c b/environment.c
index 3fd4b10845..c76d78b693 100644
--- a/environment.c
+++ b/environment.c
@@ -66,6 +66,7 @@ int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
unsigned long pack_size_limit_cfg;
enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET;
+int core_fsmonitor;
#ifndef PROTECT_HFS_DEFAULT
#define PROTECT_HFS_DEFAULT 0
diff --git a/fsmonitor.c b/fsmonitor.c
new file mode 100644
index 0000000000..ca8a6875ae
--- /dev/null
+++ b/fsmonitor.c
@@ -0,0 +1,262 @@
+#include "cache.h"
+#include "config.h"
+#include "dir.h"
+#include "ewah/ewok.h"
+#include "run-command.h"
+#include "strbuf.h"
+#include "fsmonitor.h"
+
+#define INDEX_EXTENSION_VERSION 1
+#define HOOK_INTERFACE_VERSION 1
+
+int read_fsmonitor_extension(struct index_state *istate, const void *data,
+ unsigned long sz)
+{
+ const char *index = data;
+ uint32_t hdr_version;
+ uint32_t ewah_size;
+ int ret;
+
+ if (sz < sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint32_t))
+ return error("corrupt fsmonitor extension (too short)");
+
+ hdr_version = get_be32(index);
+ index += sizeof(uint32_t);
+ if (hdr_version != INDEX_EXTENSION_VERSION)
+ return error("bad fsmonitor version %d", hdr_version);
+
+ istate->fsmonitor_last_update = get_be64(index);
+ index += sizeof(uint64_t);
+
+ ewah_size = get_be32(index);
+ index += sizeof(uint32_t);
+
+ istate->fsmonitor_dirty = ewah_new();
+ ret = ewah_read_mmap(istate->fsmonitor_dirty, index, ewah_size);
+ if (ret != ewah_size) {
+ ewah_free(istate->fsmonitor_dirty);
+ istate->fsmonitor_dirty = NULL;
+ return error("failed to parse ewah bitmap reading fsmonitor index extension");
+ }
+
+ return 0;
+}
+
+void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
+{
+ uint32_t hdr_version;
+ uint64_t tm;
+ struct ewah_bitmap *bitmap;
+ int i;
+ uint32_t ewah_start;
+ uint32_t ewah_size = 0;
+ int fixup = 0;
+
+ hdr_version = htonl(INDEX_EXTENSION_VERSION);
+ strbuf_add(sb, &hdr_version, sizeof(uint32_t));
+
+ tm = htonll((uint64_t)istate->fsmonitor_last_update);
+ strbuf_add(sb, &tm, sizeof(uint64_t));
+ fixup = sb->len;
+ strbuf_add(sb, &ewah_size, sizeof(uint32_t)); /* we'll fix this up later */
+
+ ewah_start = sb->len;
+ bitmap = ewah_new();
+ for (i = 0; i < istate->cache_nr; i++)
+ if (istate->cache[i]->ce_flags & CE_FSMONITOR_DIRTY)
+ ewah_set(bitmap, i);
+ ewah_serialize_strbuf(bitmap, sb);
+ ewah_free(bitmap);
+
+ /* fix up size field */
+ ewah_size = htonl(sb->len - ewah_start);
+ memcpy(sb->buf + fixup, &ewah_size, sizeof(uint32_t));
+}
+
+static struct untracked_cache_dir *find_untracked_cache_dir(
+ struct untracked_cache *uc, struct untracked_cache_dir *ucd,
+ const char *name)
+{
+ const char *end;
+ struct untracked_cache_dir *dir = ucd;
+
+ if (!*name)
+ return dir;
+
+ end = strchr(name, '/');
+ if (end) {
+ dir = lookup_untracked(uc, ucd, name, end - name);
+ if (dir)
+ return find_untracked_cache_dir(uc, dir, end + 1);
+ }
+
+ return dir;
+}
+
+/* This function will be passed to ewah_each_bit() */
+static void mark_fsmonitor_dirty(size_t pos, void *is)
+{
+ struct index_state *istate = is;
+ struct untracked_cache_dir *dir;
+ struct cache_entry *ce = istate->cache[pos];
+
+ assert(pos < istate->cache_nr);
+ ce->ce_flags |= CE_FSMONITOR_DIRTY;
+
+ if (!istate->untracked || !istate->untracked->root)
+ return;
+
+ dir = find_untracked_cache_dir(istate->untracked, istate->untracked->root, ce->name);
+ if (dir)
+ dir->valid = 0;
+}
+
+void tweak_fsmonitor_extension(struct index_state *istate)
+{
+ int val, fsmonitor = 0;
+
+ if (!git_config_get_maybe_bool("core.fsmonitor", &val))
+ fsmonitor = val;
+
+ if (fsmonitor) {
+ if (!istate->fsmonitor_last_update)
+ istate->cache_changed |= FSMONITOR_CHANGED;
+ if (istate->fsmonitor_dirty)
+ ewah_each_bit(istate->fsmonitor_dirty, mark_fsmonitor_dirty, istate);
+ } else {
+ if (istate->fsmonitor_last_update)
+ istate->cache_changed |= FSMONITOR_CHANGED;
+ istate->fsmonitor_last_update = 0;
+ }
+
+ if (istate->fsmonitor_dirty) {
+ ewah_free(istate->fsmonitor_dirty);
+ istate->fsmonitor_dirty = NULL;
+ }
+}
+
+/*
+ * Call the query-fsmonitor hook passing the time of the last saved results.
+ */
+static int query_fsmonitor(int version, uint64_t last_update, struct strbuf *query_result)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+ char ver[64];
+ char date[64];
+ const char *argv[4];
+
+ if (!(argv[0] = find_hook("query-fsmonitor")))
+ return -1;
+
+ snprintf(ver, sizeof(version), "%d", version);
+ snprintf(date, sizeof(date), "%" PRIuMAX, (uintmax_t)last_update);
+ argv[1] = ver;
+ argv[2] = date;
+ argv[3] = NULL;
+ cp.argv = argv;
+ cp.out = -1;
+
+ return capture_command(&cp, query_result, 1024);
+}
+
+static void mark_file_dirty(struct index_state *istate, const char *name)
+{
+ struct untracked_cache_dir *dir;
+ int pos;
+
+ /* find it in the index and mark that entry as dirty */
+ pos = index_name_pos(istate, name, strlen(name));
+ if (pos >= 0) {
+ if (!(istate->cache[pos]->ce_flags & CE_FSMONITOR_DIRTY)) {
+ istate->cache[pos]->ce_flags |= CE_FSMONITOR_DIRTY;
+ istate->cache_changed |= FSMONITOR_CHANGED;
+ }
+ }
+
+ /*
+ * Find the corresponding directory in the untracked cache
+ * and mark it as invalid
+ */
+ if (!istate->untracked || !istate->untracked->root)
+ return;
+
+ dir = find_untracked_cache_dir(istate->untracked, istate->untracked->root, name);
+ if (dir) {
+ if (dir->valid) {
+ dir->valid = 0;
+ istate->cache_changed |= FSMONITOR_CHANGED;
+ }
+ }
+}
+
+void refresh_by_fsmonitor(struct index_state *istate)
+{
+ static int has_run_once = 0;
+ struct strbuf query_result = STRBUF_INIT;
+ int query_success = 0;
+ size_t bol = 0; /* beginning of line */
+ uint64_t last_update;
+ char *buf, *entry;
+ int i;
+
+ if (!core_fsmonitor || has_run_once)
+ return;
+ has_run_once = 1;
+
+ /*
+ * This could be racy so save the date/time now and the hook
+ * should be inclusive to ensure we don't miss potential changes.
+ */
+ last_update = getnanotime();
+
+ /*
+ * If we have a last update time, call query-monitor for the set of
+ * changes since that time.
+ */
+ if (istate->fsmonitor_last_update) {
+ query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION,
+ istate->fsmonitor_last_update, &query_result);
+ trace_performance_since(last_update, "query-fsmonitor");
+ }
+
+ if (query_success) {
+ /* Mark all entries returned by the monitor as dirty */
+ buf = entry = query_result.buf;
+ for (i = 0; i < query_result.len; i++) {
+ if (buf[i] != '\0')
+ continue;
+ mark_file_dirty(istate, buf + bol);
+ bol = i + 1;
+ }
+ if (bol < query_result.len)
+ mark_file_dirty(istate, buf + bol);
+
+ /* Mark all clean entries up-to-date */
+ for (i = 0; i < istate->cache_nr; i++) {
+ struct cache_entry *ce = istate->cache[i];
+ if (ce_stage(ce) || (ce->ce_flags & CE_FSMONITOR_DIRTY))
+ continue;
+ ce_mark_uptodate(ce);
+ }
+
+ /*
+ * Now that we've marked the invalid entries in the
+ * untracked-cache itself, we can mark the untracked cache for
+ * fsmonitor usage.
+ */
+ if (istate->untracked)
+ istate->untracked->use_fsmonitor = 1;
+ } else {
+ /* if we can't update the cache, fall back to checking them all */
+ for (i = 0; i < istate->cache_nr; i++)
+ istate->cache[i]->ce_flags |= CE_FSMONITOR_DIRTY;
+
+ /* mark the untracked cache as unusable for fsmonitor */
+ if (istate->untracked)
+ istate->untracked->use_fsmonitor = 0;
+ }
+ strbuf_release(&query_result);
+
+ /* Now that we've updated istate, save the last_update time */
+ istate->fsmonitor_last_update = last_update;
+}
diff --git a/fsmonitor.h b/fsmonitor.h
new file mode 100644
index 0000000000..9c1e2b480f
--- /dev/null
+++ b/fsmonitor.h
@@ -0,0 +1,9 @@
+#ifndef FSMONITOR_H
+#define FSMONITOR_H
+
+int read_fsmonitor_extension(struct index_state *istate, const void *data, unsigned long sz);
+void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate);
+void tweak_fsmonitor_extension(struct index_state *istate);
+void refresh_by_fsmonitor(struct index_state *istate);
+
+#endif
diff --git a/read-cache.c b/read-cache.c
index 2121b6e7bb..4ffbc352c9 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -19,6 +19,7 @@
#include "varint.h"
#include "split-index.h"
#include "utf8.h"
+#include "fsmonitor.h"
/* Mask for the name length in ce_flags in the on-disk index */
@@ -38,11 +39,12 @@
#define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */
#define CACHE_EXT_LINK 0x6c696e6b /* "link" */
#define CACHE_EXT_UNTRACKED 0x554E5452 /* "UNTR" */
+#define CACHE_EXT_FSMONITOR 0x46534D4E /* "FSMN" */
/* changes that can be kept in $GIT_DIR/index (basically all extensions) */
#define EXTMASK (RESOLVE_UNDO_CHANGED | CACHE_TREE_CHANGED | \
CE_ENTRY_ADDED | CE_ENTRY_REMOVED | CE_ENTRY_CHANGED | \
- SPLIT_INDEX_ORDERED | UNTRACKED_CHANGED)
+ SPLIT_INDEX_ORDERED | UNTRACKED_CHANGED | FSMONITOR_CHANGED)
struct index_state the_index;
static const char *alternate_index_output;
@@ -62,6 +64,7 @@ static void replace_index_entry(struct index_state *istate, int nr, struct cache
free(old);
set_index_entry(istate, nr, ce);
ce->ce_flags |= CE_UPDATE_IN_BASE;
+ ce->ce_flags |= CE_FSMONITOR_DIRTY;
istate->cache_changed |= CE_ENTRY_CHANGED;
}
@@ -778,6 +781,7 @@ int chmod_index_entry(struct index_state *istate, struct cache_entry *ce,
}
cache_tree_invalidate_path(istate, ce->name);
ce->ce_flags |= CE_UPDATE_IN_BASE;
+ ce->ce_flags |= CE_FSMONITOR_DIRTY;
istate->cache_changed |= CE_ENTRY_CHANGED;
return 0;
@@ -1345,6 +1349,8 @@ int refresh_index(struct index_state *istate, unsigned int flags,
const char *added_fmt;
const char *unmerged_fmt;
+ refresh_by_fsmonitor(istate);
+
modified_fmt = (in_porcelain ? "M\t%s\n" : "%s: needs update\n");
deleted_fmt = (in_porcelain ? "D\t%s\n" : "%s: needs update\n");
typechange_fmt = (in_porcelain ? "T\t%s\n" : "%s needs update\n");
@@ -1381,8 +1387,11 @@ int refresh_index(struct index_state *istate, unsigned int flags,
continue;
new = refresh_cache_ent(istate, ce, options, &cache_errno, &changed);
- if (new == ce)
+ if (new == ce) {
+ ce->ce_flags &= ~CE_FSMONITOR_DIRTY;
continue;
+ }
+
if (!new) {
const char *fmt;
@@ -1392,6 +1401,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
*/
ce->ce_flags &= ~CE_VALID;
ce->ce_flags |= CE_UPDATE_IN_BASE;
+ ce->ce_flags |= CE_FSMONITOR_DIRTY;
istate->cache_changed |= CE_ENTRY_CHANGED;
}
if (quiet)
@@ -1550,6 +1560,9 @@ static int read_index_extension(struct index_state *istate,
case CACHE_EXT_UNTRACKED:
istate->untracked = read_untracked_extension(data, sz);
break;
+ case CACHE_EXT_FSMONITOR:
+ read_fsmonitor_extension(istate, data, sz);
+ break;
default:
if (*ext < 'A' || 'Z' < *ext)
return error("index uses %.4s extension, which we do not understand",
@@ -1722,6 +1735,7 @@ static void post_read_index_from(struct index_state *istate)
check_ce_order(istate);
tweak_untracked_cache(istate);
tweak_split_index(istate);
+ tweak_fsmonitor_extension(istate);
}
/* remember to discard_cache() before reading a different cache! */
@@ -2301,6 +2315,16 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
if (err)
return -1;
}
+ if (!strip_extensions && istate->fsmonitor_last_update) {
+ struct strbuf sb = STRBUF_INIT;
+
+ write_fsmonitor_extension(&sb, istate);
+ err = write_index_ext_header(&c, newfd, CACHE_EXT_FSMONITOR, sb.len) < 0
+ || ce_write(&c, newfd, sb.buf, sb.len) < 0;
+ strbuf_release(&sb);
+ if (err)
+ return -1;
+ }
if (ce_flush(&c, newfd, istate->sha1))
return -1;
diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh
new file mode 100755
index 0000000000..458eabe6dc
--- /dev/null
+++ b/t/t7519-status-fsmonitor.sh
@@ -0,0 +1,173 @@
+#!/bin/sh
+
+test_description='git status with file system watcher'
+
+. ./test-lib.sh
+
+clean_repo () {
+ git reset --hard HEAD &&
+ git clean -fd &&
+ rm -f marker
+}
+
+dirty_repo () {
+ : >untracked &&
+ : >dir1/untracked &&
+ : >dir2/untracked &&
+ echo 1 >modified &&
+ echo 2 >dir1/modified &&
+ echo 3 >dir2/modified &&
+ echo 4 >new &&
+ echo 5 >dir1/new &&
+ echo 6 >dir2/new &&
+ git add new &&
+ git add dir1/new &&
+ git add dir2/new
+}
+
+# The test query-fsmonitor hook proc will output a marker file we can use to
+# ensure the hook was actually used to generate the correct results.
+
+# fsmonitor works correctly with or without the untracked cache
+# but if it is available, we'll turn it on to ensure we test that
+# codepath as well.
+
+test_lazy_prereq UNTRACKED_CACHE '
+ { git update-index --test-untracked-cache; ret=$?; } &&
+ test $ret -ne 1
+'
+
+if test_have_prereq UNTRACKED_CACHE; then
+ git config core.untrackedcache true
+else
+ git config core.untrackedcache false
+fi
+
+test_expect_success 'setup' '
+ mkdir -p .git/hooks &&
+ : >tracked &&
+ : >modified &&
+ mkdir dir1 &&
+ : >dir1/tracked &&
+ : >dir1/modified &&
+ mkdir dir2 &&
+ : >dir2/tracked &&
+ : >dir2/modified &&
+ git add . &&
+ test_tick &&
+ git commit -m initial &&
+ git config core.fsmonitor true &&
+ cat >.gitignore <<-\EOF
+ .gitignore
+ expect*
+ output*
+ marker*
+ EOF
+'
+
+# Ensure commands that call refresh_index() to move the index back in time
+# properly invalidate the fsmonitor cache
+
+test_expect_success 'refresh_index() invalidates fsmonitor cache' '
+ git status &&
+ test_path_is_missing marker &&
+ dirty_repo &&
+ write_script .git/hooks/query-fsmonitor<<-\EOF &&
+ :>marker
+ EOF
+ git add . &&
+ git commit -m "to reset" &&
+ git status &&
+ test_path_is_file marker &&
+ git reset HEAD~1 &&
+ rm -f marker &&
+ git status >output &&
+ test_path_is_file marker &&
+ git -c core.fsmonitor=false status >expect &&
+ test_i18ncmp expect output
+'
+
+# Now make sure it's actually skipping the check for modified and untracked
+# files unless it is told about them. Note, after "git reset --hard HEAD" no
+# extensions exist other than 'TREE' so do a "git status" to get the extension
+# written before testing the results.
+
+test_expect_success "status doesn't detect unreported modifications" '
+ write_script .git/hooks/query-fsmonitor<<-\EOF &&
+ :>marker
+ EOF
+ clean_repo &&
+ git status &&
+ test_path_is_missing marker &&
+ : >untracked &&
+ echo 2 >dir1/modified &&
+ git status >output &&
+ test_path_is_file marker &&
+ test_i18ngrep ! "Changes not staged for commit:" output &&
+ test_i18ngrep ! "Untracked files:" output &&
+ write_script .git/hooks/query-fsmonitor<<-\EOF &&
+ :>marker
+ printf "untracked\0"
+ printf "dir1/modified\0"
+ EOF
+ rm -f marker &&
+ git status >output &&
+ test_path_is_file marker &&
+ test_i18ngrep "Changes not staged for commit:" output &&
+ test_i18ngrep "Untracked files:" output
+'
+
+# Status is well tested elsewhere so we'll just ensure that the results are
+# the same when using core.fsmonitor. First call after turning on the option
+# does a complete scan so we need to do two calls to ensure we test the new
+# codepath.
+
+test_expect_success 'status with core.untrackedcache false' '
+ git config core.untrackedcache false &&
+ write_script .git/hooks/query-fsmonitor<<-\EOF &&
+ if [ $1 -ne 1 ]
+ then
+ echo -e "Unsupported query-fsmonitor hook version.\n" >&2
+ exit 1;
+ fi
+ : >marker
+ printf "untracked\0"
+ printf "dir1/untracked\0"
+ printf "dir2/untracked\0"
+ printf "modified\0"
+ printf "dir1/modified\0"
+ printf "dir2/modified\0"
+ printf "new\0""
+ printf "dir1/new\0"
+ printf "dir2/new\0"
+ EOF
+ clean_repo &&
+ dirty_repo &&
+ git -c core.fsmonitor=false status >expect &&
+ clean_repo &&
+ git status &&
+ test_path_is_missing marker &&
+ dirty_repo &&
+ git status >output &&
+ test_path_is_file marker &&
+ test_i18ncmp expect output
+'
+
+if ! test_have_prereq UNTRACKED_CACHE; then
+ skip_all='This system does not support untracked cache'
+ test_done
+fi
+
+test_expect_success 'status with core.untrackedcache true' '
+ git config core.untrackedcache true &&
+ git -c core.fsmonitor=false status >expect &&
+ clean_repo &&
+ git status &&
+ test_path_is_missing marker &&
+ dirty_repo &&
+ git status >output &&
+ test_path_is_file marker &&
+ test_i18ncmp expect output
+'
+
+test_done
diff --git a/templates/hooks--query-fsmonitor.sample b/templates/hooks--query-fsmonitor.sample
new file mode 100755
index 0000000000..8d05b87a90
--- /dev/null
+++ b/templates/hooks--query-fsmonitor.sample
@@ -0,0 +1,76 @@
+#!/bin/sh
+#
+# An example hook script to integrate Watchman
+# (https://facebook.github.io/watchman/) with git to provide fast
+# git status.
+#
+# The hook is passed a version (currently 1) and a time in nanoseconds
+# formatted as a string and outputs to stdout all files that have been
+# modified since the given time. Paths must be relative to the root of
+# the working tree and separated by a single NUL.
+#
+# To enable this hook, rename this file to "query-fsmonitor"
+
+# check the hook interface version
+if [ "$1" -eq 1 ]
+then
+ # convert nanoseconds to seconds
+ time_t=$(($2/1000000000))
+else
+ echo -e "Unsupported query-fsmonitor hook version.\nFalling back to scanning...\n" >&2
+ exit 1;
+fi
+
+# Convert unix style paths to what Watchman expects
+case "$(uname -s)" in
+MINGW*|MSYS_NT*)
+ GIT_WORK_TREE="$(cygpath -aw "$PWD" | sed 's,\\,/,g')"
+ ;;
+*)
+ GIT_WORK_TREE="$PWD"
+ ;;
+esac
+
+# In the query expression below we're asking for names of files that
+# changed since $time_t but were not transient (ie created after
+# $time_t but no longer exist).
+#
+# To accomplish this, we're using the "since" generator to use the
+# recency index to select candidate nodes and "fields" to limit the
+# output to file names only. Then we're using the "expression" term to
+# further constrain the results.
+#
+# The category of transient files that we want to ignore will have a
+# creation clock (cclock) newer than $time_t value and will also not
+# currently exist.
+
+echo "[\"query\", \"$GIT_WORK_TREE\", { \
+ \"since\": $time_t, \
+ \"fields\": [\"name\"], \
+ \"expression\": [\"not\", [\"allof\", [\"since\", $time_t, \"cclock\"], [\"not\", \"exists\"]]] \
+ }]" | \
+ watchman -j |
+ perl -0666 -e '
+ use strict;
+ use warnings;
+
+ my $stdin = <>;
+ die "Watchman: command returned no output.\nFalling back to scanning...\n" if $stdin eq "";
+ die "Watchman: command returned invalid output: $stdin\nFalling back to scanning...\n" unless $stdin =~ /^\{/;
+
+ my $json_pkg;
+ eval {
+ require JSON::XS;
+ $json_pkg = "JSON::XS";
+ 1;
+ } or do {
+ require JSON::PP;
+ $json_pkg = "JSON::PP";
+ };
+
+ my $o = $json_pkg->new->utf8->decode($stdin);
+ die "Watchman: $o->{error}.\nFalling back to scanning...\n" if $o->{error};
+
+ local $, = "\0";
+ print @{$o->{files}};
+ '
diff --git a/unpack-trees.c b/unpack-trees.c
index 862cfce661..59a06efc37 100644
--- a/unpack-trees.c
+++ b/unpack-trees.c
@@ -420,6 +420,7 @@ static int apply_sparse_checkout(struct index_state *istate,
ce->ce_flags &= ~CE_SKIP_WORKTREE;
if (was_skip_worktree != ce_skip_worktree(ce)) {
ce->ce_flags |= CE_UPDATE_IN_BASE;
+ ce->ce_flags |= CE_FSMONITOR_DIRTY;
istate->cache_changed |= CE_ENTRY_CHANGED;
}