|
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 } |