diff options
author | Junio C Hamano <gitster@pobox.com> | 2017-07-05 15:12:17 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2017-07-05 15:12:17 -0700 |
commit | d1f9377166b226f023574a6b44066b83e0b06cab (patch) | |
tree | 43225b01b71a16117c812796a9b2bfde93469490 | |
parent | 3cfb922c005ddf2f0112a958d960fb8df3e32d4e (diff) | |
parent | 06fe0892d855943b0ecf76103cd0104e8a64782b (diff) | |
download | git-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.txt | 7 | ||||
-rw-r--r-- | Documentation/githooks.txt | 23 | ||||
-rw-r--r-- | Documentation/technical/index-format.txt | 19 | ||||
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | builtin/update-index.c | 1 | ||||
-rw-r--r-- | cache.h | 5 | ||||
-rw-r--r-- | compat/bswap.h | 4 | ||||
-rw-r--r-- | config.c | 4 | ||||
-rw-r--r-- | dir.c | 29 | ||||
-rw-r--r-- | dir.h | 5 | ||||
-rw-r--r-- | entry.c | 1 | ||||
-rw-r--r-- | environment.c | 1 | ||||
-rw-r--r-- | fsmonitor.c | 262 | ||||
-rw-r--r-- | fsmonitor.h | 9 | ||||
-rw-r--r-- | read-cache.c | 28 | ||||
-rwxr-xr-x | t/t7519-status-fsmonitor.sh | 173 | ||||
-rwxr-xr-x | templates/hooks--query-fsmonitor.sample | 76 | ||||
-rw-r--r-- | unpack-trees.c | 1 |
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. @@ -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; @@ -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; \ @@ -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; } @@ -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) { @@ -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 @@ -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; } |