Skip to content

Commit e1fd592

Browse files
committed
fsmonitor: add fsmonitor hook scripts for version 2
Version 2 of the fsmonitor hooks is passed the version and an update token and must pass back a last update token to use for subsequent calls to the hook. Signed-off-by: Kevin Willford <[email protected]>
1 parent f969c4b commit e1fd592

File tree

3 files changed

+281
-51
lines changed

3 files changed

+281
-51
lines changed

t/t7519/fsmonitor-all-v2

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/perl
2+
3+
use strict;
4+
use warnings;
5+
#
6+
# An test hook script to integrate with git to test fsmonitor.
7+
#
8+
# The hook is passed a version (currently 2) and since token
9+
# formatted as a string and outputs to stdout all files that have been
10+
# modified since the given time. Paths must be relative to the root of
11+
# the working tree and separated by a single NUL.
12+
#
13+
#echo "$0 $*" >&2
14+
my ($version, $last_update_token) = @ARGV;
15+
16+
if ($version ne 2) {
17+
print "Unsupported query-fsmonitor hook version '$version'.\n";
18+
exit 1;
19+
}
20+
21+
print "last_update_token\0/\0"

t/t7519/fsmonitor-watchman-v2

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/perl
2+
3+
use strict;
4+
use warnings;
5+
use IPC::Open2;
6+
7+
# An example hook script to integrate Watchman
8+
# (https://facebook.github.io/watchman/) with git to speed up detecting
9+
# new and modified files.
10+
#
11+
# The hook is passed a version (currently 2) and last update token
12+
# formatted as a string and outputs to stdout a new update token and
13+
# all files that have been modified since the update token. Paths must
14+
# be relative to the root of the working tree and separated by a single NUL.
15+
#
16+
# To enable this hook, rename this file to "query-watchman" and set
17+
# 'git config core.fsmonitor .git/hooks/query-watchman'
18+
#
19+
my ($version, $last_update_token) = @ARGV;
20+
21+
# Uncomment for debugging
22+
# print STDERR "$0 $version $last_update_token\n";
23+
24+
# Check the hook interface version
25+
if ($version ne 2) {
26+
die "Unsupported query-fsmonitor hook version '$version'.\n" .
27+
"Falling back to scanning...\n";
28+
}
29+
30+
my $git_work_tree = get_working_dir();
31+
32+
my $retry = 1;
33+
34+
my $json_pkg;
35+
eval {
36+
require JSON::XS;
37+
$json_pkg = "JSON::XS";
38+
1;
39+
} or do {
40+
require JSON::PP;
41+
$json_pkg = "JSON::PP";
42+
};
43+
44+
launch_watchman();
45+
46+
sub launch_watchman {
47+
my $o = watchman_query();
48+
if (is_work_tree_watched($o)) {
49+
output_result($o->{clock}, @{$o->{files}});
50+
}
51+
}
52+
53+
sub output_result {
54+
my ($clockid, @files) = @_;
55+
56+
# Uncomment for debugging watchman output
57+
# open (my $fh, ">", ".git/watchman-output.out");
58+
# binmode $fh, ":utf8";
59+
# print $fh "$clockid\n@files\n";
60+
# close $fh;
61+
62+
binmode STDOUT, ":utf8";
63+
print $clockid;
64+
print "\0";
65+
local $, = "\0";
66+
print @files;
67+
}
68+
69+
sub watchman_clock {
70+
my $response = qx/watchman clock "$git_work_tree"/;
71+
die "Failed to get clock id on '$git_work_tree'.\n" .
72+
"Falling back to scanning...\n" if $? != 0;
73+
74+
return $json_pkg->new->utf8->decode($response);
75+
}
76+
77+
sub watchman_query {
78+
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
79+
or die "open2() failed: $!\n" .
80+
"Falling back to scanning...\n";
81+
82+
# In the query expression below we're asking for names of files that
83+
# changed since $last_update_token but not from the .git folder.
84+
#
85+
# To accomplish this, we're using the "since" generator to use the
86+
# recency index to select candidate nodes and "fields" to limit the
87+
# output to file names only. Then we're using the "expression" term to
88+
# further constrain the results.
89+
if (substr($last_update_token, 0, 1) eq "c") {
90+
$last_update_token = "\"$last_update_token\"";
91+
}
92+
my $query = <<" END";
93+
["query", "$git_work_tree", {
94+
"since": $last_update_token,
95+
"fields": ["name"],
96+
"expression": ["not", ["dirname", ".git"]]
97+
}]
98+
END
99+
100+
# Uncomment for debugging the watchman query
101+
# open (my $fh, ">", ".git/watchman-query.json");
102+
# print $fh $query;
103+
# close $fh;
104+
105+
print CHLD_IN $query;
106+
close CHLD_IN;
107+
my $response = do {local $/; <CHLD_OUT>};
108+
109+
# Uncomment for debugging the watch response
110+
# open ($fh, ">", ".git/watchman-response.json");
111+
# print $fh $response;
112+
# close $fh;
113+
114+
die "Watchman: command returned no output.\n" .
115+
"Falling back to scanning...\n" if $response eq "";
116+
die "Watchman: command returned invalid output: $response\n" .
117+
"Falling back to scanning...\n" unless $response =~ /^\{/;
118+
119+
return $json_pkg->new->utf8->decode($response);
120+
}
121+
122+
sub is_work_tree_watched {
123+
my ($output) = @_;
124+
my $error = $output->{error};
125+
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
126+
$retry--;
127+
my $response = qx/watchman watch "$git_work_tree"/;
128+
die "Failed to make watchman watch '$git_work_tree'.\n" .
129+
"Falling back to scanning...\n" if $? != 0;
130+
$output = $json_pkg->new->utf8->decode($response);
131+
$error = $output->{error};
132+
die "Watchman: $error.\n" .
133+
"Falling back to scanning...\n" if $error;
134+
135+
# Uncomment for debugging watchman output
136+
# open (my $fh, ">", ".git/watchman-output.out");
137+
# close $fh;
138+
139+
# Watchman will always return all files on the first query so
140+
# return the fast "everything is dirty" flag to git and do the
141+
# Watchman query just to get it over with now so we won't pay
142+
# the cost in git to look up each individual file.
143+
my $o = watchman_clock();
144+
$error = $output->{error};
145+
146+
die "Watchman: $error.\n" .
147+
"Falling back to scanning...\n" if $error;
148+
149+
output_result($o->{clock}, ("/"));
150+
$last_update_token = $o->{clock};
151+
152+
eval { launch_watchman() };
153+
return 0;
154+
}
155+
156+
die "Watchman: $error.\n" .
157+
"Falling back to scanning...\n" if $error;
158+
159+
return 1;
160+
}
161+
162+
sub get_working_dir {
163+
my $working_dir;
164+
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
165+
$working_dir = Win32::GetCwd();
166+
$working_dir =~ tr/\\/\//;
167+
} else {
168+
require Cwd;
169+
$working_dir = Cwd::cwd();
170+
}
171+
172+
return $working_dir;
173+
}

templates/hooks--fsmonitor-watchman.sample

Lines changed: 87 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,83 @@ use IPC::Open2;
88
# (https://facebook.github.io/watchman/) with git to speed up detecting
99
# new and modified files.
1010
#
11-
# The hook is passed a version (currently 1) and a time in nanoseconds
12-
# formatted as a string and outputs to stdout all files that have been
13-
# modified since the given time. Paths must be relative to the root of
14-
# the working tree and separated by a single NUL.
11+
# The hook is passed a version (currently 2) and last update token
12+
# formatted as a string and outputs to stdout a new update token and
13+
# all files that have been modified since the update token. Paths must
14+
# be relative to the root of the working tree and separated by a single NUL.
1515
#
1616
# To enable this hook, rename this file to "query-watchman" and set
1717
# 'git config core.fsmonitor .git/hooks/query-watchman'
1818
#
19-
my ($version, $time) = @ARGV;
19+
my ($version, $last_update_token) = @ARGV;
2020

2121
# Check the hook interface version
22-
23-
if ($version == 1) {
24-
# convert nanoseconds to seconds
25-
# subtract one second to make sure watchman will return all changes
26-
$time = int ($time / 1000000000) - 1;
27-
} else {
22+
if ($version ne 2) {
2823
die "Unsupported query-fsmonitor hook version '$version'.\n" .
2924
"Falling back to scanning...\n";
3025
}
3126

32-
my $git_work_tree;
33-
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
34-
$git_work_tree = Win32::GetCwd();
35-
$git_work_tree =~ tr/\\/\//;
36-
} else {
37-
require Cwd;
38-
$git_work_tree = Cwd::cwd();
39-
}
27+
my $git_work_tree = get_working_dir();
4028

4129
my $retry = 1;
4230

31+
my $json_pkg;
32+
eval {
33+
require JSON::XS;
34+
$json_pkg = "JSON::XS";
35+
1;
36+
} or do {
37+
require JSON::PP;
38+
$json_pkg = "JSON::PP";
39+
};
40+
4341
launch_watchman();
4442

4543
sub launch_watchman {
44+
$o = watchman_query();
45+
if (is_work_tree_watched($o)) {
46+
output_result($o->{clock}, @{$o->{files}});
47+
}
48+
}
49+
50+
sub output_result {
51+
my ($clockid, @files) = @_;
52+
53+
binmode STDOUT, ":utf8";
54+
print $clockid;
55+
print "\0";
56+
local $, = "\0";
57+
print @files;
58+
}
59+
60+
sub watchman_clock {
61+
my $response = qx/watchman clock "$git_work_tree"/;
62+
die "Failed to get clock id on '$git_work_tree'.\n" .
63+
"Falling back to scanning...\n" if $? != 0;
4664

65+
return $json_pkg->new->utf8->decode($response);
66+
}
67+
68+
sub watchman_query {
4769
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
48-
or die "open2() failed: $!\n" .
49-
"Falling back to scanning...\n";
70+
or die "open2() failed: $!\n" .
71+
"Falling back to scanning...\n";
5072

5173
# In the query expression below we're asking for names of files that
52-
# changed since $time but were not transient (ie created after
53-
# $time but no longer exist).
74+
# changed since $last_update_token but not from the .git folder.
5475
#
5576
# To accomplish this, we're using the "since" generator to use the
5677
# recency index to select candidate nodes and "fields" to limit the
57-
# output to file names only.
58-
78+
# output to file names only. Then we're using the "expression" term to
79+
# further constrain the results.
80+
if (substr($last_update_token, 0, 1) eq "c") {
81+
$last_update_token = "\"$last_update_token\"";
82+
}
5983
my $query = <<" END";
6084
["query", "$git_work_tree", {
61-
"since": $time,
62-
"fields": ["name"]
85+
"since": $last_update_token,
86+
"fields": ["name"],
87+
"expression": ["not", ["dirname", ".git"]]
6388
}]
6489
END
6590

@@ -68,24 +93,16 @@ sub launch_watchman {
6893
my $response = do {local $/; <CHLD_OUT>};
6994

7095
die "Watchman: command returned no output.\n" .
71-
"Falling back to scanning...\n" if $response eq "";
96+
"Falling back to scanning...\n" if $response eq "";
7297
die "Watchman: command returned invalid output: $response\n" .
73-
"Falling back to scanning...\n" unless $response =~ /^\{/;
74-
75-
my $json_pkg;
76-
eval {
77-
require JSON::XS;
78-
$json_pkg = "JSON::XS";
79-
1;
80-
} or do {
81-
require JSON::PP;
82-
$json_pkg = "JSON::PP";
83-
};
84-
85-
my $o = $json_pkg->new->utf8->decode($response);
86-
87-
if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
88-
print STDERR "Adding '$git_work_tree' to watchman's watch list.\n";
98+
"Falling back to scanning...\n" unless $response =~ /^\{/;
99+
100+
return $json_pkg->new->utf8->decode($response);
101+
}
102+
103+
sub is_work_tree_watched {
104+
my ($output) = @_;
105+
if ($retry > 0 and $output->{error} and $output->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
89106
$retry--;
90107
qx/watchman watch "$git_work_tree"/;
91108
die "Failed to make watchman watch '$git_work_tree'.\n" .
@@ -95,15 +112,34 @@ sub launch_watchman {
95112
# return the fast "everything is dirty" flag to git and do the
96113
# Watchman query just to get it over with now so we won't pay
97114
# the cost in git to look up each individual file.
98-
print "/\0";
115+
my $o = watchman_clock();
116+
$error = $output->{error};
117+
118+
die "Watchman: $error.\n" .
119+
"Falling back to scanning...\n" if $error;
120+
121+
output_result($o->{clock}, ("/"));
122+
$last_update_token = $o->{clock};
123+
99124
eval { launch_watchman() };
100-
exit 0;
125+
return 0;
101126
}
102127

103-
die "Watchman: $o->{error}.\n" .
104-
"Falling back to scanning...\n" if $o->{error};
128+
die "Watchman: $output->{error}.\n" .
129+
"Falling back to scanning...\n" if $output->{error};
105130

106-
binmode STDOUT, ":utf8";
107-
local $, = "\0";
108-
print @{$o->{files}};
131+
return 1;
132+
}
133+
134+
sub get_working_dir {
135+
my $working_dir;
136+
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
137+
$working_dir = Win32::GetCwd();
138+
$working_dir =~ tr/\\/\//;
139+
} else {
140+
require Cwd;
141+
$working_dir = Cwd::cwd();
142+
}
143+
144+
return $working_dir;
109145
}

0 commit comments

Comments
 (0)