Merge branch 'kw/fsmonitor-watchman-racefix'

A new version of fsmonitor-watchman hook has been introduced, to
avoid races.

* kw/fsmonitor-watchman-racefix:
  fsmonitor: update documentation for hook version and watchman hooks
  fsmonitor: add fsmonitor hook scripts for version 2
  fsmonitor: handle version 2 of the hooks that will use opaque token
  fsmonitor: change last update timestamp on the index_state to opaque token
This commit is contained in:
Junio C Hamano
2020-02-14 12:54:20 -08:00
11 changed files with 432 additions and 85 deletions

View File

@ -68,6 +68,17 @@ core.fsmonitor::
avoiding unnecessary processing of files that have not changed. avoiding unnecessary processing of files that have not changed.
See the "fsmonitor-watchman" section of linkgit:githooks[5]. See the "fsmonitor-watchman" section of linkgit:githooks[5].
core.fsmonitorHookVersion::
Sets the version of hook that is to be used when calling fsmonitor.
There are currently versions 1 and 2. When this is not set,
version 2 will be tried first and if it fails then version 1
will be tried. Version 1 uses a timestamp as input to determine
which files have changes since that time but some monitors
like watchman have race conditions when used with a timestamp.
Version 2 uses an opaque string so that the monitor can return
something that can be used to determine what files have changed
without race conditions.
core.trustctime:: core.trustctime::
If false, the ctime differences between the index and the If false, the ctime differences between the index and the
working tree are ignored; useful when the inode change time working tree are ignored; useful when the inode change time

View File

@ -490,9 +490,16 @@ fsmonitor-watchman
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
This hook is invoked when the configuration option `core.fsmonitor` is This hook is invoked when the configuration option `core.fsmonitor` is
set to `.git/hooks/fsmonitor-watchman`. It takes two arguments, a version set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`
(currently 1) and the time in elapsed nanoseconds since midnight, depending on the version of the hook to use.
January 1, 1970.
Version 1 takes two arguments, a version (1) and the time in elapsed
nanoseconds since midnight, January 1, 1970.
Version 2 takes two arguments, a version (2) and a token that is used
for identifying changes since the token. For watchman this would be
a clock id. This version must output to stdout the new token followed
by a NUL before the list of files.
The hook should output to stdout the list of all files in the working 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 directory that may have changed since the requested time. The logic

View File

@ -324,7 +324,7 @@ struct index_state {
struct hashmap dir_hash; struct hashmap dir_hash;
struct object_id oid; struct object_id oid;
struct untracked_cache *untracked; struct untracked_cache *untracked;
uint64_t fsmonitor_last_update; char *fsmonitor_last_update;
struct ewah_bitmap *fsmonitor_dirty; struct ewah_bitmap *fsmonitor_dirty;
struct mem_pool *ce_mem_pool; struct mem_pool *ce_mem_pool;
struct progress *progress; struct progress *progress;

View File

@ -6,8 +6,10 @@
#include "run-command.h" #include "run-command.h"
#include "strbuf.h" #include "strbuf.h"
#define INDEX_EXTENSION_VERSION (1) #define INDEX_EXTENSION_VERSION1 (1)
#define HOOK_INTERFACE_VERSION (1) #define INDEX_EXTENSION_VERSION2 (2)
#define HOOK_INTERFACE_VERSION1 (1)
#define HOOK_INTERFACE_VERSION2 (2)
struct trace_key trace_fsmonitor = TRACE_KEY_INIT(FSMONITOR); struct trace_key trace_fsmonitor = TRACE_KEY_INIT(FSMONITOR);
@ -24,6 +26,22 @@ static void fsmonitor_ewah_callback(size_t pos, void *is)
ce->ce_flags &= ~CE_FSMONITOR_VALID; ce->ce_flags &= ~CE_FSMONITOR_VALID;
} }
static int fsmonitor_hook_version(void)
{
int hook_version;
if (git_config_get_int("core.fsmonitorhookversion", &hook_version))
return -1;
if (hook_version == HOOK_INTERFACE_VERSION1 ||
hook_version == HOOK_INTERFACE_VERSION2)
return hook_version;
warning("Invalid hook version '%i' in core.fsmonitorhookversion. "
"Must be 1 or 2.", hook_version);
return -1;
}
int read_fsmonitor_extension(struct index_state *istate, const void *data, int read_fsmonitor_extension(struct index_state *istate, const void *data,
unsigned long sz) unsigned long sz)
{ {
@ -32,17 +50,26 @@ int read_fsmonitor_extension(struct index_state *istate, const void *data,
uint32_t ewah_size; uint32_t ewah_size;
struct ewah_bitmap *fsmonitor_dirty; struct ewah_bitmap *fsmonitor_dirty;
int ret; int ret;
uint64_t timestamp;
struct strbuf last_update = STRBUF_INIT;
if (sz < sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint32_t)) if (sz < sizeof(uint32_t) + 1 + sizeof(uint32_t))
return error("corrupt fsmonitor extension (too short)"); return error("corrupt fsmonitor extension (too short)");
hdr_version = get_be32(index); hdr_version = get_be32(index);
index += sizeof(uint32_t); index += sizeof(uint32_t);
if (hdr_version != INDEX_EXTENSION_VERSION) if (hdr_version == INDEX_EXTENSION_VERSION1) {
timestamp = get_be64(index);
strbuf_addf(&last_update, "%"PRIu64"", timestamp);
index += sizeof(uint64_t);
} else if (hdr_version == INDEX_EXTENSION_VERSION2) {
strbuf_addstr(&last_update, index);
index += last_update.len + 1;
} else {
return error("bad fsmonitor version %d", hdr_version); return error("bad fsmonitor version %d", hdr_version);
}
istate->fsmonitor_last_update = get_be64(index); istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL);
index += sizeof(uint64_t);
ewah_size = get_be32(index); ewah_size = get_be32(index);
index += sizeof(uint32_t); index += sizeof(uint32_t);
@ -79,7 +106,6 @@ void fill_fsmonitor_bitmap(struct index_state *istate)
void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate) void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
{ {
uint32_t hdr_version; uint32_t hdr_version;
uint64_t tm;
uint32_t ewah_start; uint32_t ewah_start;
uint32_t ewah_size = 0; uint32_t ewah_size = 0;
int fixup = 0; int fixup = 0;
@ -89,11 +115,12 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
BUG("fsmonitor_dirty has more entries than the index (%"PRIuMAX" > %u)", BUG("fsmonitor_dirty has more entries than the index (%"PRIuMAX" > %u)",
(uintmax_t)istate->fsmonitor_dirty->bit_size, istate->cache_nr); (uintmax_t)istate->fsmonitor_dirty->bit_size, istate->cache_nr);
put_be32(&hdr_version, INDEX_EXTENSION_VERSION); put_be32(&hdr_version, INDEX_EXTENSION_VERSION2);
strbuf_add(sb, &hdr_version, sizeof(uint32_t)); strbuf_add(sb, &hdr_version, sizeof(uint32_t));
put_be64(&tm, istate->fsmonitor_last_update); strbuf_addstr(sb, istate->fsmonitor_last_update);
strbuf_add(sb, &tm, sizeof(uint64_t)); strbuf_addch(sb, 0); /* Want to keep a NUL */
fixup = sb->len; fixup = sb->len;
strbuf_add(sb, &ewah_size, sizeof(uint32_t)); /* we'll fix this up later */ strbuf_add(sb, &ewah_size, sizeof(uint32_t)); /* we'll fix this up later */
@ -110,9 +137,9 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
} }
/* /*
* Call the query-fsmonitor hook passing the time of the last saved results. * Call the query-fsmonitor hook passing the last update token of the saved results.
*/ */
static int query_fsmonitor(int version, uint64_t last_update, struct strbuf *query_result) static int query_fsmonitor(int version, const char *last_update, struct strbuf *query_result)
{ {
struct child_process cp = CHILD_PROCESS_INIT; struct child_process cp = CHILD_PROCESS_INIT;
@ -121,7 +148,7 @@ static int query_fsmonitor(int version, uint64_t last_update, struct strbuf *que
argv_array_push(&cp.args, core_fsmonitor); argv_array_push(&cp.args, core_fsmonitor);
argv_array_pushf(&cp.args, "%d", version); argv_array_pushf(&cp.args, "%d", version);
argv_array_pushf(&cp.args, "%" PRIuMAX, (uintmax_t)last_update); argv_array_pushf(&cp.args, "%s", last_update);
cp.use_shell = 1; cp.use_shell = 1;
cp.dir = get_git_work_tree(); cp.dir = get_git_work_tree();
@ -148,14 +175,18 @@ static void fsmonitor_refresh_callback(struct index_state *istate, const char *n
void refresh_fsmonitor(struct index_state *istate) void refresh_fsmonitor(struct index_state *istate)
{ {
struct strbuf query_result = STRBUF_INIT; struct strbuf query_result = STRBUF_INIT;
int query_success = 0; int query_success = 0, hook_version = -1;
size_t bol; /* beginning of line */ size_t bol = 0; /* beginning of line */
uint64_t last_update; uint64_t last_update;
struct strbuf last_update_token = STRBUF_INIT;
char *buf; char *buf;
unsigned int i; unsigned int i;
if (!core_fsmonitor || istate->fsmonitor_has_run_once) if (!core_fsmonitor || istate->fsmonitor_has_run_once)
return; return;
hook_version = fsmonitor_hook_version();
istate->fsmonitor_has_run_once = 1; istate->fsmonitor_has_run_once = 1;
trace_printf_key(&trace_fsmonitor, "refresh fsmonitor"); trace_printf_key(&trace_fsmonitor, "refresh fsmonitor");
@ -164,26 +195,60 @@ void refresh_fsmonitor(struct index_state *istate)
* should be inclusive to ensure we don't miss potential changes. * should be inclusive to ensure we don't miss potential changes.
*/ */
last_update = getnanotime(); last_update = getnanotime();
if (hook_version == HOOK_INTERFACE_VERSION1)
strbuf_addf(&last_update_token, "%"PRIu64"", last_update);
/* /*
* If we have a last update time, call query_fsmonitor for the set of * If we have a last update token, call query_fsmonitor for the set of
* changes since that time, else assume everything is possibly dirty * changes since that token, else assume everything is possibly dirty
* and check it all. * and check it all.
*/ */
if (istate->fsmonitor_last_update) { if (istate->fsmonitor_last_update) {
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION, if (hook_version == -1 || hook_version == HOOK_INTERFACE_VERSION2) {
istate->fsmonitor_last_update, &query_result); query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION2,
istate->fsmonitor_last_update, &query_result);
if (query_success) {
if (hook_version < 0)
hook_version = HOOK_INTERFACE_VERSION2;
/*
* First entry will be the last update token
* Need to use a char * variable because static
* analysis was suggesting to use strbuf_addbuf
* but we don't want to copy the entire strbuf
* only the the chars up to the first NUL
*/
buf = query_result.buf;
strbuf_addstr(&last_update_token, buf);
if (!last_update_token.len) {
warning("Empty last update token.");
query_success = 0;
} else {
bol = last_update_token.len + 1;
}
} else if (hook_version < 0) {
hook_version = HOOK_INTERFACE_VERSION1;
if (!last_update_token.len)
strbuf_addf(&last_update_token, "%"PRIu64"", last_update);
}
}
if (hook_version == HOOK_INTERFACE_VERSION1) {
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION1,
istate->fsmonitor_last_update, &query_result);
}
trace_performance_since(last_update, "fsmonitor process '%s'", core_fsmonitor); trace_performance_since(last_update, "fsmonitor process '%s'", core_fsmonitor);
trace_printf_key(&trace_fsmonitor, "fsmonitor process '%s' returned %s", trace_printf_key(&trace_fsmonitor, "fsmonitor process '%s' returned %s",
core_fsmonitor, query_success ? "success" : "failure"); core_fsmonitor, query_success ? "success" : "failure");
} }
/* a fsmonitor process can return '/' to indicate all entries are invalid */ /* a fsmonitor process can return '/' to indicate all entries are invalid */
if (query_success && query_result.buf[0] != '/') { if (query_success && query_result.buf[bol] != '/') {
/* Mark all entries returned by the monitor as dirty */ /* Mark all entries returned by the monitor as dirty */
buf = query_result.buf; buf = query_result.buf;
bol = 0; for (i = bol; i < query_result.len; i++) {
for (i = 0; i < query_result.len; i++) {
if (buf[i] != '\0') if (buf[i] != '\0')
continue; continue;
fsmonitor_refresh_callback(istate, buf + bol); fsmonitor_refresh_callback(istate, buf + bol);
@ -217,18 +282,21 @@ void refresh_fsmonitor(struct index_state *istate)
} }
strbuf_release(&query_result); strbuf_release(&query_result);
/* Now that we've updated istate, save the last_update time */ /* Now that we've updated istate, save the last_update_token */
istate->fsmonitor_last_update = last_update; FREE_AND_NULL(istate->fsmonitor_last_update);
istate->fsmonitor_last_update = strbuf_detach(&last_update_token, NULL);
} }
void add_fsmonitor(struct index_state *istate) void add_fsmonitor(struct index_state *istate)
{ {
unsigned int i; unsigned int i;
struct strbuf last_update = STRBUF_INIT;
if (!istate->fsmonitor_last_update) { if (!istate->fsmonitor_last_update) {
trace_printf_key(&trace_fsmonitor, "add fsmonitor"); trace_printf_key(&trace_fsmonitor, "add fsmonitor");
istate->cache_changed |= FSMONITOR_CHANGED; istate->cache_changed |= FSMONITOR_CHANGED;
istate->fsmonitor_last_update = getnanotime(); strbuf_addf(&last_update, "%"PRIu64"", getnanotime());
istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL);
/* reset the fsmonitor state */ /* reset the fsmonitor state */
for (i = 0; i < istate->cache_nr; i++) for (i = 0; i < istate->cache_nr; i++)
@ -250,7 +318,7 @@ void remove_fsmonitor(struct index_state *istate)
if (istate->fsmonitor_last_update) { if (istate->fsmonitor_last_update) {
trace_printf_key(&trace_fsmonitor, "remove fsmonitor"); trace_printf_key(&trace_fsmonitor, "remove fsmonitor");
istate->cache_changed |= FSMONITOR_CHANGED; istate->cache_changed |= FSMONITOR_CHANGED;
istate->fsmonitor_last_update = 0; FREE_AND_NULL(istate->fsmonitor_last_update);
} }
} }

View File

@ -13,7 +13,7 @@ int cmd__dump_fsmonitor(int ac, const char **av)
printf("no fsmonitor\n"); printf("no fsmonitor\n");
return 0; return 0;
} }
printf("fsmonitor last update %"PRIuMAX"\n", (uintmax_t)istate->fsmonitor_last_update); printf("fsmonitor last update %s\n", istate->fsmonitor_last_update);
for (i = 0; i < istate->cache_nr; i++) for (i = 0; i < istate->cache_nr; i++)
printf((istate->cache[i]->ce_flags & CE_FSMONITOR_VALID) ? "+" : "-"); printf((istate->cache[i]->ce_flags & CE_FSMONITOR_VALID) ? "+" : "-");

View File

@ -32,11 +32,12 @@ write_integration_script () {
echo "$0: exactly 2 arguments expected" echo "$0: exactly 2 arguments expected"
exit 2 exit 2
fi fi
if test "$1" != 1 if test "$1" != 2
then then
echo "Unsupported core.fsmonitor hook version." >&2 echo "Unsupported core.fsmonitor hook version." >&2
exit 1 exit 1
fi fi
printf "last_update_token\0"
printf "untracked\0" printf "untracked\0"
printf "dir1/untracked\0" printf "dir1/untracked\0"
printf "dir2/untracked\0" printf "dir2/untracked\0"
@ -107,6 +108,7 @@ EOF
# test that "update-index --fsmonitor-valid" sets the fsmonitor valid bit # test that "update-index --fsmonitor-valid" sets the fsmonitor valid bit
test_expect_success 'update-index --fsmonitor-valid" sets the fsmonitor valid bit' ' test_expect_success 'update-index --fsmonitor-valid" sets the fsmonitor valid bit' '
write_script .git/hooks/fsmonitor-test<<-\EOF && write_script .git/hooks/fsmonitor-test<<-\EOF &&
printf "last_update_token\0"
EOF EOF
git update-index --fsmonitor && git update-index --fsmonitor &&
git update-index --fsmonitor-valid dir1/modified && git update-index --fsmonitor-valid dir1/modified &&
@ -167,6 +169,7 @@ EOF
# test that newly added files are marked valid # test that newly added files are marked valid
test_expect_success 'newly added files are marked valid' ' test_expect_success 'newly added files are marked valid' '
write_script .git/hooks/fsmonitor-test<<-\EOF && write_script .git/hooks/fsmonitor-test<<-\EOF &&
printf "last_update_token\0"
EOF EOF
git add new && git add new &&
git add dir1/new && git add dir1/new &&
@ -207,6 +210,7 @@ EOF
# test that *only* files returned by the integration script get flagged as invalid # test that *only* files returned by the integration script get flagged as invalid
test_expect_success '*only* files returned by the integration script get flagged as invalid' ' test_expect_success '*only* files returned by the integration script get flagged as invalid' '
write_script .git/hooks/fsmonitor-test<<-\EOF && write_script .git/hooks/fsmonitor-test<<-\EOF &&
printf "last_update_token\0"
printf "dir1/modified\0" printf "dir1/modified\0"
EOF EOF
clean_repo && clean_repo &&
@ -276,6 +280,7 @@ do
# (if enabled) files unless it is told about them. # (if enabled) files unless it is told about them.
test_expect_success "status doesn't detect unreported modifications" ' test_expect_success "status doesn't detect unreported modifications" '
write_script .git/hooks/fsmonitor-test<<-\EOF && write_script .git/hooks/fsmonitor-test<<-\EOF &&
printf "last_update_token\0"
:>marker :>marker
EOF EOF
clean_repo && clean_repo &&

View File

@ -17,7 +17,6 @@ fi
if test "$1" != 1 if test "$1" != 1
then then
echo "Unsupported core.fsmonitor hook version." >&2
exit 1 exit 1
fi fi

21
t/t7519/fsmonitor-all-v2 Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/perl
use strict;
use warnings;
#
# An test hook script to integrate with git to test fsmonitor.
#
# The hook is passed a version (currently 2) and since token
# 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.
#
#echo "$0 $*" >&2
my ($version, $last_update_token) = @ARGV;
if ($version ne 2) {
print "Unsupported query-fsmonitor hook version '$version'.\n";
exit 1;
}
print "last_update_token\0/\0"

View File

@ -26,8 +26,7 @@ if ($version == 1) {
# subtract one second to make sure watchman will return all changes # subtract one second to make sure watchman will return all changes
$time = int ($time / 1000000000) - 1; $time = int ($time / 1000000000) - 1;
} else { } else {
die "Unsupported query-fsmonitor hook version '$version'.\n" . exit 1;
"Falling back to scanning...\n";
} }
my $git_work_tree; my $git_work_tree;

173
t/t7519/fsmonitor-watchman-v2 Executable file
View File

@ -0,0 +1,173 @@
#!/usr/bin/perl
use strict;
use warnings;
use IPC::Open2;
# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout a new update token and
# all files that have been modified since the update token. 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-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version
if ($version ne 2) {
die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n";
}
my $git_work_tree = get_working_dir();
my $retry = 1;
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman();
sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" .
"Falling back to scanning...\n";
# In the query expression below we're asking for names of files that
# changed since $last_update_token but not from the .git folder.
#
# 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.
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
}
my $query = <<" END";
["query", "$git_work_tree", {
"since": $last_update_token,
"fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}]
END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query;
close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/;
return $json_pkg->new->utf8->decode($response);
}
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
$retry--;
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
$last_update_token = $o->{clock};
eval { launch_watchman() };
return 0;
}
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
return 1;
}
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
}

View File

@ -8,102 +8,166 @@ use IPC::Open2;
# (https://facebook.github.io/watchman/) with git to speed up detecting # (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files. # new and modified files.
# #
# The hook is passed a version (currently 1) and a time in nanoseconds # The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout all files that have been # formatted as a string and outputs to stdout a new update token and
# modified since the given time. Paths must be relative to the root of # all files that have been modified since the update token. Paths must
# the working tree and separated by a single NUL. # be relative to the root of the working tree and separated by a single NUL.
# #
# To enable this hook, rename this file to "query-watchman" and set # To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman' # 'git config core.fsmonitor .git/hooks/query-watchman'
# #
my ($version, $time) = @ARGV; my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version # Check the hook interface version
if ($version ne 2) {
if ($version == 1) {
# convert nanoseconds to seconds
# subtract one second to make sure watchman will return all changes
$time = int ($time / 1000000000) - 1;
} else {
die "Unsupported query-fsmonitor hook version '$version'.\n" . die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n"; "Falling back to scanning...\n";
} }
my $git_work_tree; my $git_work_tree = get_working_dir();
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$git_work_tree = Win32::GetCwd();
$git_work_tree =~ tr/\\/\//;
} else {
require Cwd;
$git_work_tree = Cwd::cwd();
}
my $retry = 1; my $retry = 1;
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman(); launch_watchman();
sub launch_watchman { sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" . or die "open2() failed: $!\n" .
"Falling back to scanning...\n"; "Falling back to scanning...\n";
# In the query expression below we're asking for names of files that # In the query expression below we're asking for names of files that
# changed since $time but were not transient (ie created after # changed since $last_update_token but not from the .git folder.
# $time but no longer exist).
# #
# To accomplish this, we're using the "since" generator to use the # To accomplish this, we're using the "since" generator to use the
# recency index to select candidate nodes and "fields" to limit the # recency index to select candidate nodes and "fields" to limit the
# output to file names only. # output to file names only. Then we're using the "expression" term to
# further constrain the results.
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
}
my $query = <<" END"; my $query = <<" END";
["query", "$git_work_tree", { ["query", "$git_work_tree", {
"since": $time, "since": $last_update_token,
"fields": ["name"] "fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}] }]
END END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query; print CHLD_IN $query;
close CHLD_IN; close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>}; my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" . die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq ""; "Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" . die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/; "Falling back to scanning...\n" unless $response =~ /^\{/;
my $json_pkg; return $json_pkg->new->utf8->decode($response);
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($response); sub is_work_tree_watched {
my ($output) = @_;
if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { my $error = $output->{error};
print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
$retry--; $retry--;
qx/watchman watch "$git_work_tree"/; my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" . die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0; "Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so # Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the # return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay # Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file. # the cost in git to look up each individual file.
print "/\0"; my $o = watchman_clock();
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
$last_update_token = $o->{clock};
eval { launch_watchman() }; eval { launch_watchman() };
exit 0; return 0;
} }
die "Watchman: $o->{error}.\n" . die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $o->{error}; "Falling back to scanning...\n" if $error;
binmode STDOUT, ":utf8"; return 1;
local $, = "\0"; }
print @{$o->{files}};
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
} }