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 <Kevin.Willford@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Kevin Willford committed
Jan 23, 2020 at 15:26 UTC
e4e1e8342aa03c4e4585908736a103130cc8906d
3 files changed
+310
-52
t/t7519/fsmonitor-all-v2
new
+21
@@ -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
new
+173
@@ -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
+116
-52
@@ -8,102 +8,166 @@ use IPC::Open2;
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 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.
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, $time) = @ARGV;
19
+my ($version, $last_update_token) = @ARGV;
20
21
-# Check the hook interface version
21
+# Uncomment for debugging
22
+# print STDERR "$0 $version $last_update_token\n";
23
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 {
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
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
-}
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')
48
- or die "open2() failed: $!\n" .
49
- "Falling back to scanning...\n";
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
52
- # changed since $time but were not transient (ie created after
53
- # $time but no longer exist).
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
57
- # output to file names only.
58
-
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", {
61
- "since": $time,
62
- "fields": ["name"]
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" .
71
- "Falling back to scanning...\n" if $response eq "";
115
+ "Falling back to scanning...\n" if $response eq "";
116
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";
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--;
90
- qx/watchman watch "$git_work_tree"/;
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.
98
- print "/\0";
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() };
100
- exit 0;
153
+ return 0;
154
}
155
103
- die "Watchman: $o->{error}.\n" .
104
- "Falling back to scanning...\n" if $o->{error};
156
+ die "Watchman: $error.\n" .
157
+ "Falling back to scanning...\n" if $error;
158
106
- binmode STDOUT, ":utf8";
107
- local $, = "\0";
108
- print @{$o->{files}};
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
}