@matheusbsbs / matheus-bagliano / commits / e4e1e8342a
Fork 0

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 }