From: "Paul Tarjan via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Koji Nakamaru <koji.nakamaru@gree.net>,
"brian m. carlson" <sandals@crustytoothpaste.net>,
Paul Tarjan <github@paulisageek.com>,
Paul Tarjan <github@paulisageek.com>
Subject: [PATCH v2] fsmonitor-watchman: fix variable reference and remove redundant code
Date: Sat, 28 Feb 2026 17:37:57 +0000 [thread overview]
Message-ID: <pull.2180.v2.git.git.1772300277959.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2180.git.git.1769391202338.gitgitgadget@gmail.com>
From: Paul Tarjan <github@paulisageek.com>
The is_work_tree_watched() function in fsmonitor-watchman.sample has
two bugs:
1. Wrong variable in error check: After calling watchman_clock(), the
result is stored in $o, but the code checks $output->{error} instead
of $o->{error}. This means errors from the clock command are silently
ignored.
2. Double output violates protocol: When the retry path triggers (the
directory wasn't initially watched), output_result() is called with
the "/" flag, then launch_watchman() is called recursively which
calls output_result() again. This outputs two clock tokens to stdout,
but git's fsmonitor v2 protocol expects exactly one response.
Fix #1 by checking $o->{error} after watchman_clock().
Fix #2 by removing the recursive launch_watchman() call. The "/"
"everything is dirty" flag already tells git to do a full scan, and
git will call the hook again on the next invocation with a valid clock
token.
With the recursive call removed, the $retry guard is no longer needed
since it only existed to prevent infinite recursion. Remove it.
Apply the same fixes to the test helper scripts in t/t7519/.
Signed-off-by: Paul Tarjan <github@paulisageek.com>
---
fsmonitor-watchman: fix variable reference and remove redundant code
fsmonitor-watchman: fix variable reference and remove redundant code
The is_work_tree_watched() function in fsmonitor-watchman.sample has two
bugs:
1. Wrong variable in error check: After calling watchman_clock(), the
result is stored in $o, but the code checks $output->{error} instead
of $o->{error}. This means errors from the clock command are
silently ignored.
2. Double output violates protocol: When the retry path triggers (the
directory wasn't initially watched), output_result() is called with
the "/" flag, then launch_watchman() is called recursively which
calls output_result() again. This outputs two clock tokens to
stdout, but git's fsmonitor v2 protocol expects exactly one
response.
Fix #1 by checking $o->{error} after watchman_clock().
Fix #2 by removing the recursive launch_watchman() call. The "/"
"everything is dirty" flag already tells git to do a full scan, and git
will call the hook again on the next invocation with a valid clock
token.
Apply the same fixes to the test helper scripts in t/t7519/.
Changes since v1:
* Removed $retry variable and associated logic, which only existed to
prevent infinite recursion from the now-removed recursive
launch_watchman() call
* Fixed commit authorship
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2180%2Fptarjan%2Fclaude%2Ffix-watchman-query-bug-sfbIw-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2180/ptarjan/claude/fix-watchman-query-bug-sfbIw-v2
Pull-Request: https://github.com/git/git/pull/2180
Range-diff vs v1:
1: 116d26287f ! 1: b9e00b8ab5 fsmonitor: fix two bugs in watchman hook retry path
@@
## Metadata ##
-Author: Claude <noreply@anthropic.com>
+Author: Paul Tarjan <github@paulisageek.com>
## Commit message ##
- fsmonitor: fix two bugs in watchman hook retry path
+ fsmonitor-watchman: fix variable reference and remove redundant code
The is_work_tree_watched() function in fsmonitor-watchman.sample has
two bugs:
@@ Commit message
git will call the hook again on the next invocation with a valid clock
token.
+ With the recursive call removed, the $retry guard is no longer needed
+ since it only existed to prevent infinite recursion. Remove it.
+
Apply the same fixes to the test helper scripts in t/t7519/.
Signed-off-by: Paul Tarjan <github@paulisageek.com>
## t/t7519/fsmonitor-watchman ##
+@@ t/t7519/fsmonitor-watchman: if ($^O =~ 'msys' || $^O =~ 'cygwin') {
+ $git_work_tree = Cwd::cwd();
+ }
+
+-my $retry = 1;
+-
+ launch_watchman();
+
+ sub launch_watchman {
+@@ t/t7519/fsmonitor-watchman: sub launch_watchman {
+
+ my $o = $json_pkg->new->utf8->decode($response);
+
+- if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
++ if ($o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
+ print STDERR "Adding '$git_work_tree' to watchman's watch list.\n";
+- $retry--;
+ qx/watchman watch "$git_work_tree"/;
+ die "Failed to make watchman watch '$git_work_tree'.\n" .
+ "Falling back to scanning...\n" if $? != 0;
@@ t/t7519/fsmonitor-watchman: sub launch_watchman {
close $fh;
@@ t/t7519/fsmonitor-watchman: sub launch_watchman {
## t/t7519/fsmonitor-watchman-v2 ##
+@@ t/t7519/fsmonitor-watchman-v2: if ($version ne 2) {
+
+ my $git_work_tree = get_working_dir();
+
+-my $retry = 1;
+-
+ my $json_pkg;
+ eval {
+ require JSON::XS;
+@@ t/t7519/fsmonitor-watchman-v2: sub watchman_query {
+ sub is_work_tree_watched {
+ my ($output) = @_;
+ my $error = $output->{error};
+- if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
+- $retry--;
++ if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
+ my $response = qx/watchman watch "$git_work_tree"/;
+ die "Failed to make watchman watch '$git_work_tree'.\n" .
+ "Falling back to scanning...\n" if $? != 0;
@@ t/t7519/fsmonitor-watchman-v2: sub is_work_tree_watched {
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
@@ t/t7519/fsmonitor-watchman-v2: sub is_work_tree_watched {
## templates/hooks/fsmonitor-watchman.sample ##
+@@ templates/hooks/fsmonitor-watchman.sample: if ($version ne 2) {
+
+ my $git_work_tree = get_working_dir();
+
+-my $retry = 1;
+-
+ my $json_pkg;
+ eval {
+ require JSON::XS;
+@@ templates/hooks/fsmonitor-watchman.sample: sub watchman_query {
+ sub is_work_tree_watched {
+ my ($output) = @_;
+ my $error = $output->{error};
+- if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
+- $retry--;
++ if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
+ my $response = qx/watchman watch "$git_work_tree"/;
+ die "Failed to make watchman watch '$git_work_tree'.\n" .
+ "Falling back to scanning...\n" if $? != 0;
@@ templates/hooks/fsmonitor-watchman.sample: sub is_work_tree_watched {
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
t/t7519/fsmonitor-watchman | 6 +-----
t/t7519/fsmonitor-watchman-v2 | 10 ++--------
templates/hooks/fsmonitor-watchman.sample | 10 ++--------
3 files changed, 5 insertions(+), 21 deletions(-)
diff --git a/t/t7519/fsmonitor-watchman b/t/t7519/fsmonitor-watchman
index 264b9daf83..bcc055c1e0 100755
--- a/t/t7519/fsmonitor-watchman
+++ b/t/t7519/fsmonitor-watchman
@@ -38,8 +38,6 @@ if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$git_work_tree = Cwd::cwd();
}
-my $retry = 1;
-
launch_watchman();
sub launch_watchman {
@@ -92,9 +90,8 @@ sub launch_watchman {
my $o = $json_pkg->new->utf8->decode($response);
- if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
+ if ($o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
print STDERR "Adding '$git_work_tree' to watchman's watch list.\n";
- $retry--;
qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
@@ -109,7 +106,6 @@ sub launch_watchman {
close $fh;
print "/\0";
- eval { launch_watchman() };
exit 0;
}
diff --git a/t/t7519/fsmonitor-watchman-v2 b/t/t7519/fsmonitor-watchman-v2
index 14ed0aa42d..368604c278 100755
--- a/t/t7519/fsmonitor-watchman-v2
+++ b/t/t7519/fsmonitor-watchman-v2
@@ -29,8 +29,6 @@ if ($version ne 2) {
my $git_work_tree = get_working_dir();
-my $retry = 1;
-
my $json_pkg;
eval {
require JSON::XS;
@@ -122,8 +120,7 @@ sub watchman_query {
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
- if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
- $retry--;
+ if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
@@ -141,15 +138,12 @@ sub is_work_tree_watched {
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
- $error = $output->{error};
+ $error = $o->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
- $last_update_token = $o->{clock};
-
- eval { launch_watchman() };
return 0;
}
diff --git a/templates/hooks/fsmonitor-watchman.sample b/templates/hooks/fsmonitor-watchman.sample
index 23e856f5de..429e0a51c1 100755
--- a/templates/hooks/fsmonitor-watchman.sample
+++ b/templates/hooks/fsmonitor-watchman.sample
@@ -29,8 +29,6 @@ if ($version ne 2) {
my $git_work_tree = get_working_dir();
-my $retry = 1;
-
my $json_pkg;
eval {
require JSON::XS;
@@ -123,8 +121,7 @@ sub watchman_query {
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
- if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
- $retry--;
+ if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
@@ -142,15 +139,12 @@ sub is_work_tree_watched {
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
- $error = $output->{error};
+ $error = $o->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
- $last_update_token = $o->{clock};
-
- eval { launch_watchman() };
return 0;
}
base-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe
--
gitgitgadget
prev parent reply other threads:[~2026-02-28 17:38 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-26 1:33 [PATCH] fsmonitor: fix two bugs in watchman hook retry path Paul Tarjan via GitGitGadget
2026-02-08 6:03 ` Paul Tarjan
2026-02-27 6:25 ` [PATCH] fsmonitor-watchman: fix variable reference and remove redundant code Paul Tarjan
2026-02-28 17:02 ` [PATCH] fsmonitor: fix two bugs in watchman hook retry path Koji Nakamaru
2026-02-28 17:33 ` [PATCH] fsmonitor-watchman: fix variable reference and remove redundant code Paul Tarjan
2026-02-28 17:15 ` [PATCH] fsmonitor: fix two bugs in watchman hook retry path brian m. carlson
2026-02-28 17:33 ` [PATCH] fsmonitor-watchman: fix variable reference and remove redundant code Paul Tarjan
2026-02-28 17:37 ` Paul Tarjan via GitGitGadget [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2180.v2.git.git.1772300277959.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=github@paulisageek.com \
--cc=koji.nakamaru@gree.net \
--cc=sandals@crustytoothpaste.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.