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