1#!/usr/bin/perl 2 3use strict; 4use warnings; 5use 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# 19my ($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 25if ($version ne 2) { 26 die "Unsupported query-fsmonitor hook version '$version'.\n" . 27 "Falling back to scanning...\n"; 28} 29 30my $git_work_tree = get_working_dir(); 31 32my $retry = 1; 33 34my $json_pkg; 35eval { 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 44launch_watchman(); 45 46sub launch_watchman { 47 my $o = watchman_query(); 48 if (is_work_tree_watched($o)) { 49 output_result($o->{clock}, @{$o->{files}}); 50 } 51} 52 53sub 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 69sub 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 77sub 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 my $last_update_line = ""; 90 if (substr($last_update_token, 0, 1) eq "c") { 91 $last_update_token = "\"$last_update_token\""; 92 $last_update_line = qq[\n"since": $last_update_token,]; 93 } 94 my $query = <<" END"; 95 ["query", "$git_work_tree", {$last_update_line 96 "fields": ["name"], 97 "expression": ["not", ["dirname", ".git"]] 98 }] 99 END 100 101 # Uncomment for debugging the watchman query 102 # open (my $fh, ">", ".git/watchman-query.json"); 103 # print $fh $query; 104 # close $fh; 105 106 print CHLD_IN $query; 107 close CHLD_IN; 108 my $response = do {local $/; <CHLD_OUT>}; 109 110 # Uncomment for debugging the watch response 111 # open ($fh, ">", ".git/watchman-response.json"); 112 # print $fh $response; 113 # close $fh; 114 115 die "Watchman: command returned no output.\n" . 116 "Falling back to scanning...\n" if $response eq ""; 117 die "Watchman: command returned invalid output: $response\n" . 118 "Falling back to scanning...\n" unless $response =~ /^\{/; 119 120 return $json_pkg->new->utf8->decode($response); 121} 122 123sub is_work_tree_watched { 124 my ($output) = @_; 125 my $error = $output->{error}; 126 if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { 127 $retry--; 128 my $response = qx/watchman watch "$git_work_tree"/; 129 die "Failed to make watchman watch '$git_work_tree'.\n" . 130 "Falling back to scanning...\n" if $? != 0; 131 $output = $json_pkg->new->utf8->decode($response); 132 $error = $output->{error}; 133 die "Watchman: $error.\n" . 134 "Falling back to scanning...\n" if $error; 135 136 # Uncomment for debugging watchman output 137 # open (my $fh, ">", ".git/watchman-output.out"); 138 # close $fh; 139 140 # Watchman will always return all files on the first query so 141 # return the fast "everything is dirty" flag to git and do the 142 # Watchman query just to get it over with now so we won't pay 143 # the cost in git to look up each individual file. 144 my $o = watchman_clock(); 145 $error = $output->{error}; 146 147 die "Watchman: $error.\n" . 148 "Falling back to scanning...\n" if $error; 149 150 output_result($o->{clock}, ("/")); 151 $last_update_token = $o->{clock}; 152 153 eval { launch_watchman() }; 154 return 0; 155 } 156 157 die "Watchman: $error.\n" . 158 "Falling back to scanning...\n" if $error; 159 160 return 1; 161} 162 163sub get_working_dir { 164 my $working_dir; 165 if ($^O =~ 'msys' || $^O =~ 'cygwin') { 166 $working_dir = Win32::GetCwd(); 167 $working_dir =~ tr/\\/\//; 168 } else { 169 require Cwd; 170 $working_dir = Cwd::cwd(); 171 } 172 173 return $working_dir; 174} 175