.git/hooks/fsmonitor-watchman.sample
changeset 762 0147c0a9ce2b
equal deleted inserted replaced
761:e4e656f19771 762:0147c0a9ce2b
       
     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 }