git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2] fsmonitor-watchman: fix variable reference and remove redundant code

From
PGPaul Tarjan via GitGitGadget <gitgitgadget@gmail.com>
Date
Feb 28, 2026, 17:37 UTC
Message-ID
<pull.2180.v2.git.git.1772300277959.gitgitgadget@gmail.com>
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
Previous: Paul Tarjan
Message 8 of 8 in “fsmonitor: fix two bugs in watchman hook retry path”
  1. fsmonitor: fix two bugs in watchman hook retry pathPaul Tarjan via GitGitGadget, Jan 26, 2026
  2. Paul TarjanFeb 8, 2026
  3. Paul TarjanFeb 27, 2026
  4. Koji NakamaruFeb 28, 2026
  5. Paul TarjanFeb 28, 2026
  6. brian m. carlsonFeb 28, 2026
  7. Paul TarjanFeb 28, 2026
  8. fsmonitor-watchman: fix variable reference and remove redundant codePaul Tarjan via GitGitGadget, Feb 28, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.