fsmonitor: add a sample integration script for Watchman
This script integrates the new fsmonitor capabilities of git with the cross platform Watchman file watching service. To use the script: Download and install Watchman from https://facebook.github.io/watchman/. Rename the sample integration hook from fsmonitor-watchman.sample to fsmonitor-watchman. Configure git to use the extension: git config core.fsmonitor .git/hooks/fsmonitor-watchman Optionally turn on the untracked cache for optimal performance. Signed-off-by: Ben Peart <benpeart@microsoft.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Christian Couder <christian.couder@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Ben Peart committed
Sep 22, 2017 at 12:35 UTC
def4376711f607914bfb784f8de21867062fa284
1 file changed
+122
templates/hooks--fsmonitor-watchman.sample
new
+122
@@ -0,0 +1,122 @@
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 1) and a time in nanoseconds
12
+# formatted as a string and outputs to stdout all files that have been
13
+# modified since the given time. Paths must be relative to the root of
14
+# 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, $time) = @ARGV;
20
+
21
+# Check the hook interface version
22
+
23
+if ($version == 1) {
24
+ # convert nanoseconds to seconds
25
+ $time = int $time / 1000000000;
26
+} else {
27
+ die "Unsupported query-fsmonitor hook version '$version'.\n" .
28
+ "Falling back to scanning...\n";
29
+}
30
+
31
+# Convert unix style paths to escaped Windows style paths when running
32
+# in Windows command prompt
33
+
34
+my $system = `uname -s`;
35
+$system =~ s/[\r\n]+//g;
36
+my $git_work_tree;
37
+
38
+if ($system =~ m/^MSYS_NT/) {
39
+ $git_work_tree = `cygpath -aw "\$PWD"`;
40
+ $git_work_tree =~ s/[\r\n]+//g;
41
+ $git_work_tree =~ s,\\,/,g;
42
+} else {
43
+ $git_work_tree = $ENV{'PWD'};
44
+}
45
+
46
+my $retry = 1;
47
+
48
+launch_watchman();
49
+
50
+sub launch_watchman {
51
+
52
+ # Set input record separator
53
+ local $/ = 0666;
54
+
55
+ my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j')
56
+ or die "open2() failed: $!\n" .
57
+ "Falling back to scanning...\n";
58
+
59
+ # In the query expression below we're asking for names of files that
60
+ # changed since $time but were not transient (ie created after
61
+ # $time but no longer exist).
62
+ #
63
+ # To accomplish this, we're using the "since" generator to use the
64
+ # recency index to select candidate nodes and "fields" to limit the
65
+ # output to file names only. Then we're using the "expression" term to
66
+ # further constrain the results.
67
+ #
68
+ # The category of transient files that we want to ignore will have a
69
+ # creation clock (cclock) newer than $time_t value and will also not
70
+ # currently exist.
71
+
72
+ my $query = <<" END";
73
+ ["query", "$git_work_tree", {
74
+ "since": $time,
75
+ "fields": ["name"],
76
+ "expression": ["not", ["allof", ["since", $time, "cclock"], ["not", "exists"]]]
77
+ }]
78
+ END
79
+
80
+ print CHLD_IN $query;
81
+ my $response = <CHLD_OUT>;
82
+
83
+ die "Watchman: command returned no output.\n" .
84
+ "Falling back to scanning...\n" if $response eq "";
85
+ die "Watchman: command returned invalid output: $response\n" .
86
+ "Falling back to scanning...\n" unless $response =~ /^\{/;
87
+
88
+ my $json_pkg;
89
+ eval {
90
+ require JSON::XS;
91
+ $json_pkg = "JSON::XS";
92
+ 1;
93
+ } or do {
94
+ require JSON::PP;
95
+ $json_pkg = "JSON::PP";
96
+ };
97
+
98
+ my $o = $json_pkg->new->utf8->decode($response);
99
+
100
+ if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
101
+ print STDERR "Adding '$git_work_tree' to watchman's watch list.\n";
102
+ $retry--;
103
+ qx/watchman watch "$git_work_tree"/;
104
+ die "Failed to make watchman watch '$git_work_tree'.\n" .
105
+ "Falling back to scanning...\n" if $? != 0;
106
+
107
+ # Watchman will always return all files on the first query so
108
+ # return the fast "everything is dirty" flag to git and do the
109
+ # Watchman query just to get it over with now so we won't pay
110
+ # the cost in git to look up each individual file.
111
+ print "/\0";
112
+ eval { launch_watchman() };
113
+ exit 0;
114
+ }
115
+
116
+ die "Watchman: $o->{error}.\n" .
117
+ "Falling back to scanning...\n" if $o->{error};
118
+
119
+ binmode STDOUT, ":utf8";
120
+ local $, = "\0";
121
+ print @{$o->{files}};
122
+}