Format: application/octet-stream
⬇ Download File (4.5 KB)
   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  }
 174