{"thread":{"id":"64696","subject":"[PATCH] fsmonitor: implement filesystem change listener for Linux","startedAt":"2025-12-30T08:14:13Z","lastAt":"2026-05-12T20:36:49Z","messageCount":211,"participants":["Paul Tarjan via GitGitGadget","Junio C Hamano","Patrick Steinhardt","Paul Tarjan","Johannes Schindelin","SZEDER Gábor","Jeff King","Ben Knoble","D. Ben Knoble"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"532823","messageId":"pull.2147.git.git.1767082450088.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":null,"subject":"[PATCH] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T08:14:10Z","receivedAt":"2025-12-30T08:14:13Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement fsmonitor for Linux using the inotify API, bringing it to\nfeature parity with existing Windows and macOS implementations.\n\nThe Linux implementation uses inotify to monitor filesystem events.\nUnlike macOS's FSEvents which can watch a single root directory,\ninotify requires registering watches on every directory of interest.\nThe implementation carefully handles directory renames and moves\nusing inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\nevent pairs.\n\nKey implementation details:\n- Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n- Maintains bidirectional hashmaps between watch descriptors and paths\n  for efficient event processing\n- Handles directory creation, deletion, and renames dynamically\n- Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n- Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n- Creates batches lazily (only for actual file events, not cookies)\n  to avoid spurious sequence number increments\n\nBuild configuration:\n- Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n- Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n- Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nDocumentation updated to note that fsmonitor.socketDir is now supported\non both Mac OS and Linux, and adds a section about inotify watch limits.\n\nTesting performed:\n- Build succeeds with standard flags and SANITIZE=address\n- All t7527-builtin-fsmonitor.sh tests pass on local filesystems\n- Remote filesystem detection correctly rejects network mounts\n\nIssues addressed from PR #1352 (git/git) review comments:\n- GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n  scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n- Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n- Unsafe hashmap iteration in dtor: Collect entries first, then modify\n- Missing null checks in stop_async: Added proper guard conditions\n- dirname() modifying argument: Create copy with xstrdup() first\n- Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n- Missing worktree null check: Added BUG() for null worktree\n- Header updates: Use git-compat-util.h, hash_to_hex_algop()\n- Code style: Use xstrdup() not xmemdupz(), proper pointer style\n\nIssues addressed from PR #1667 (git/git) review comments:\n- EINTR handling: read() now handles both EAGAIN and EINTR\n- Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n- Unchecked add_watch return: Now logs failure in rename_dir()\n- String building: Consolidated strbuf operations with strbuf_addf()\n- Translation markers: Added _() to all error_errno() messages\n\nBased on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\nand https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated\nto work with the current codebase and address all review feedback.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n    fsmonitor: implement filesystem change listener for Linux\n    \n    Implement fsmonitor for Linux using the inotify API, bringing it to\n    feature parity with existing Windows and macOS implementations.\n    \n    The Linux implementation uses inotify to monitor filesystem events.\n    Unlike macOS's FSEvents which can watch a single root directory, inotify\n    requires registering watches on every directory of interest. The\n    implementation carefully handles directory renames and moves using\n    inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO event\n    pairs.\n    \n    Key implementation details:\n    \n     * Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n     * Maintains bidirectional hashmaps between watch descriptors and paths\n       for efficient event processing\n     * Handles directory creation, deletion, and renames dynamically\n     * Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n     * Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n    \n    Build configuration:\n    \n     * Enabled via FSMONITOR_DAEMON_BACKEND=linux and\n       FSMONITOR_OS_SETTINGS=linux\n     * Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n     * Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n    \n    Documentation updated to note that fsmonitor.socketDir is now supported\n    on both Mac OS and Linux, and adds a section about inotify watch limits.\n    \n    Testing performed:\n    \n     * Build succeeds with standard flags (NO_CURL, NO_GETTEXT, NO_TCLTK)\n     * Build succeeds with SANITIZE=address (AddressSanitizer)\n     * General tests pass (t0001-init, t3200-branch, etc.)\n     * fsmonitor tests require local filesystem (v9fs test environment is\n       correctly detected as remote and rejected)\n     * Manual testing confirms daemon starts and monitors filesystem when\n       configured with fsmonitor.allowremote=true and fsmonitor.socketdir\n       pointing to a local filesystem (tmpfs)\n    \n    Issues addressed from PR #1352 (git/git) review comments:\n    \n     * GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n       scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     * Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n     * Unsafe hashmap iteration in dtor: Collect entries first, then modify\n     * Missing null checks in stop_async: Added proper guard conditions\n     * dirname() modifying argument: Create copy with xstrdup() first\n     * Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n     * Missing worktree null check: Added BUG() for null worktree\n     * Header updates: Use git-compat-util.h, hash_to_hex_algop()\n     * Code style: Use xstrdup() not xmemdupz(), proper pointer style\n    \n    Issues addressed from PR #1667 (git/git) review comments:\n    \n     * EINTR handling: read() now handles both EAGAIN and EINTR\n     * Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n     * Unchecked add_watch return: Now logs failure in rename_dir()\n     * String building: Consolidated strbuf operations with strbuf_addf()\n     * Translation markers: Added _() to all error_errno() messages\n    \n    Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n    work with the current codebase and address all review feedback.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v1\nPull-Request: https://github.com/git/git/pull/2147\n\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-ipc-linux.c            |  61 ++\n compat/fsmonitor/fsm-listen-linux.c         | 738 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 224 ++++++\n compat/fsmonitor/fsm-settings-linux.c       |  71 ++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |  10 +\n 9 files changed, 1173 insertions(+), 6 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-ipc-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n create mode 100644 compat/fsmonitor/fsm-settings-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-ipc-linux.c b/compat/fsmonitor/fsm-ipc-linux.c\nnew file mode 100644\nindex 0000000000..d34a6419bc\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-ipc-linux.c\n@@ -0,0 +1,61 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"path.h\"\n+#include \"repository.h\"\n+#include \"strbuf.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, \"fsmonitor--daemon.ipc\")\n+\n+const char *fsmonitor_ipc__get_path(struct repository *r)\n+{\n+\tstatic const char *ipc_path = NULL;\n+\tgit_SHA_CTX sha1ctx;\n+\tchar *sock_dir = NULL;\n+\tstruct strbuf ipc_file = STRBUF_INIT;\n+\tunsigned char hash[GIT_SHA1_RAWSZ];\n+\n+\tif (!r)\n+\t\tBUG(\"No repository passed into fsmonitor_ipc__get_path\");\n+\n+\tif (ipc_path)\n+\t\treturn ipc_path;\n+\n+\t/* By default the socket file is created in the .git directory */\n+\tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n+\t\tipc_path = fsmonitor_ipc__get_default_path();\n+\t\treturn ipc_path;\n+\t}\n+\n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n+\tgit_SHA1_Init(&sha1ctx);\n+\tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n+\tgit_SHA1_Final(hash, &sha1ctx);\n+\n+\trepo_config_get_string(r, \"fsmonitor.socketdir\", &sock_dir);\n+\n+\t/* Create the socket file in either socketDir or $HOME */\n+\tif (sock_dir && *sock_dir) {\n+\t\tstrbuf_addf(&ipc_file, \"%s/.git-fsmonitor-%s\",\n+\t\t\t    sock_dir, hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t} else {\n+\t\tstrbuf_addf(&ipc_file, \"~/.git-fsmonitor-%s\",\n+\t\t\t    hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t}\n+\tfree(sock_dir);\n+\n+\tipc_path = interpolate_path(ipc_file.buf, 1);\n+\tif (!ipc_path)\n+\t\tdie(_(\"Invalid path: %s\"), ipc_file.buf);\n+\n+\tstrbuf_release(&ipc_file);\n+\treturn ipc_path;\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..2593c834d3\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,738 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <poll.h>\n+#include <sys/inotify.h>\n+#include <sys/stat.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"Removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"Double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"No matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"No matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\tconst int interval = 1000;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 1);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"Missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..d0bc98b0a9\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,224 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <errno.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase V9FS_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/compat/fsmonitor/fsm-settings-linux.c b/compat/fsmonitor/fsm-settings-linux.c\nnew file mode 100644\nindex 0000000000..23e7442d0c\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-settings-linux.c\n@@ -0,0 +1,71 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-settings.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+#include <libgen.h>\n+\n+/*\n+ * For the builtin FSMonitor, we create the Unix domain socket for the\n+ * IPC in the .git directory.  If the working directory is remote,\n+ * then the socket will be created on the remote file system.  This\n+ * can fail if the remote file system does not support UDS file types\n+ * (e.g. smbfs to a Windows server) or if the remote kernel does not\n+ * allow a non-local process to bind() the socket.  (These problems\n+ * could be fixed by moving the UDS out of the .git directory and to a\n+ * well-known local directory on the client machine, but care should\n+ * be taken to ensure that $HOME is actually local and not a managed\n+ * file share.)\n+ *\n+ * FAT32 and NTFS working directories are problematic too.\n+ *\n+ * The builtin FSMonitor uses a Unix domain socket in the .git\n+ * directory for IPC.  These Windows drive formats do not support\n+ * Unix domain sockets, so mark them as incompatible for the daemon.\n+ */\n+static enum fsmonitor_reason check_uds_volume(struct repository *r)\n+{\n+\tstruct fs_info fs;\n+\tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n+\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n+\t\treturn FSMONITOR_REASON_ERROR;\n+\t}\n+\n+\tfree(path);\n+\n+\tif (fs.is_remote ||\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n+\t\tfree(fs.typename);\n+\t\treturn FSMONITOR_REASON_NOSOCKETS;\n+\t}\n+\n+\tfree(fs.typename);\n+\treturn FSMONITOR_REASON_OK;\n+}\n+\n+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)\n+{\n+\tenum fsmonitor_reason reason;\n+\n+\tif (ipc) {\n+\t\treason = check_uds_volume(r);\n+\t\tif (reason != FSMONITOR_REASON_OK)\n+\t\t\treturn reason;\n+\t}\n+\n+\treturn FSMONITOR_REASON_OK;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..a0fd0752a3 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = linux\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..141b05acb0 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -308,6 +308,16 @@ if(SUPPORTS_SIMPLE_IPC)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n+\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)\n \tendif()\n endif()\n \n\nbase-commit: 7c7698a654a7a0031f65b0ab0c1c4e438e95df60\n-- \ngitgitgadget\n"},{"id":"532825","messageId":"xmqqqzscfbcw.fsf@gitster.g","threadId":"64696","inReplyTo":"pull.2147.git.git.1767082450088.gitgitgadget@gmail.com","subject":"Re: [PATCH] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-30T11:38:39Z","receivedAt":"2025-12-30T11:38:41Z","isPatch":true,"body":"Exciting.\n\nIt seems to die with leaks when \"make SANITIZE=leak test\" is run,\nthough.\n\nInitialized empty Git repository in /home/gitster/w/git.git/t/trash directory.t7527-builtin-fsmonitor/test_implicit/.git/\nfsmonitor-daemon is not watching '/home/gitster/w/git.git/t/trash directory.t7527-builtin-fsmonitor/test_implicit'\nbuiltin:0.145521.20251230T113644.793433Z:0Q/Q\n{\"event\":\"data\",\"sid\":\"20251230T113644.762195Z-H3cfff1b1-P0002386f\",\"thread\":\"main\",\"time\":\"2025-12-30T11:36:44.813131Z\",\"file\":\"fsmonitor-ipc.c\",\"line\":99,\"t_abs\":0.052581,\"t_rel\":0.048830,\"nesting\":2,\"category\":\"fsm_client\",\"key\":\"query/response-length\",\"value\":\"45\"}\nfsmonitor-daemon is watching '/home/gitster/w/git.git/t/trash directory.t7527-builtin-fsmonitor/test_implicit'\nfsmonitor-daemon is not watching '/home/gitster/w/git.git/t/trash directory.t7527-builtin-fsmonitor/test_implicit'\nfatal: fsmonitor--daemon is not running\nnot ok 2 - implicit daemon start\n#\n#               test_when_finished \"stop_daemon_delete_repo test_implicit\" &&\n#\n#               git init test_implicit &&\n#               test_must_fail git -C test_implicit fsmonitor--daemon status &&\n#\n#               # query will implicitly start the daemon.\n#               #\n#               # for test-script simplicity, we send a V1 timestamp rather than\n#               # a V2 token.  either way, the daemon response to any query contains\n#               # a new V2 token.  (the daemon may complain that we sent a V1 request,\n#               # but this test case is only concerned with whether the daemon was\n#               # implicitly started.)\n#\n#               GIT_TRACE2_EVENT=\"$PWD/.git/trace\" \\\n#                       test-tool -C test_implicit fsmonitor-client query --token 0 >actual &&\n#               nul_to_q <actual >actual.filtered &&\n#               grep \"builtin:\" actual.filtered &&\n#\n#               # confirm that a daemon was started in the background.\n#               #\n#               # since the mechanism for starting the background daemon is platform\n#               # dependent, just confirm that the foreground command received a\n#               # response from the daemon.\n#\n#               have_t2_data_event fsm_client query/response-length <.git/trace &&\n#\n#               git -C test_implicit fsmonitor--daemon status &&\n#               git -C test_implicit fsmonitor--daemon stop &&\n#               test_must_fail git -C test_implicit fsmonitor--daemon status\n#\n1..2\n\n=================================================================\n==git==145489==ERROR: LeakSanitizer: detected memory leaks\n\nDirect leak of 512 byte(s) in 1 object(s) allocated from:\n    #0 0x56179e6ce042 in calloc (git+0x8c042) (BuildId: de5ce3c9d0b0c09380c910e6a9eb181e324abde6)\n    #1 0x56179ea0aef4 in xcalloc wrapper.c:154:8\n    #2 0x56179e8b17b7 in alloc_table hashmap.c:79:2\n    #3 0x56179e8b174c in hashmap_init hashmap.c:168:2\n    #4 0x56179e73e6fe in fsmonitor_run_daemon builtin/fsmonitor--daemon.c:1288:2\n    #5 0x56179e73e141 in try_to_run_foreground_daemon builtin/fsmonitor--daemon.c:1448:11\n    #6 0x56179e73dc44 in cmd_fsmonitor__daemon builtin/fsmonitor--daemon.c:1584:12\n    #7 0x56179e6d2c8a in run_builtin git.c:506:11\n    #8 0x56179e6d1910 in handle_builtin git.c:779:9\n    #9 0x56179e6d2747 in run_argv git.c:862:4\n    #10 0x56179e6d169b in cmd_main git.c:984:19\n    #11 0x56179e7f7a7a in main common-main.c:9:11\n    #12 0x7f091ea66ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16\n    #13 0x7f091ea66d64 in __libc_start_main csu/../csu/libc-start.c:360:3\n    #14 0x56179e69e280 in _start (git+0x5c280) (BuildId: de5ce3c9d0b0c09380c910e6a9eb181e324abde6)\n"},{"id":"532826","messageId":"pull.2147.v2.git.git.1767096494372.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.git.git.1767082450088.gitgitgadget@gmail.com","subject":"[PATCH v2] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T12:08:14Z","receivedAt":"2025-12-30T12:08:17Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement fsmonitor for Linux using the inotify API, bringing it to\nfeature parity with existing Windows and macOS implementations.\n\nThe Linux implementation uses inotify to monitor filesystem events.\nUnlike macOS's FSEvents which can watch a single root directory,\ninotify requires registering watches on every directory of interest.\nThe implementation carefully handles directory renames and moves\nusing inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\nevent pairs.\n\nKey implementation details:\n- Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n- Maintains bidirectional hashmaps between watch descriptors and paths\n  for efficient event processing\n- Handles directory creation, deletion, and renames dynamically\n- Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n- Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n- Creates batches lazily (only for actual file events, not cookies)\n  to avoid spurious sequence number increments\n\nBuild configuration:\n- Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n- Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n- Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nDocumentation updated to note that fsmonitor.socketDir is now supported\non both Mac OS and Linux, and adds a section about inotify watch limits.\n\nTesting performed:\n- Build succeeds with standard flags and SANITIZE=address\n- All t7527-builtin-fsmonitor.sh tests pass on local filesystems\n- Remote filesystem detection correctly rejects network mounts\n\nIssues addressed from PR #1352 (git/git) review comments:\n- GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n  scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n- Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n- Unsafe hashmap iteration in dtor: Collect entries first, then modify\n- Missing null checks in stop_async: Added proper guard conditions\n- dirname() modifying argument: Create copy with xstrdup() first\n- Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n- Missing worktree null check: Added BUG() for null worktree\n- Header updates: Use git-compat-util.h, hash_to_hex_algop()\n- Code style: Use xstrdup() not xmemdupz(), proper pointer style\n\nIssues addressed from PR #1667 (git/git) review comments:\n- EINTR handling: read() now handles both EAGAIN and EINTR\n- Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n- Unchecked add_watch return: Now logs failure in rename_dir()\n- String building: Consolidated strbuf operations with strbuf_addf()\n- Translation markers: Added _() to all error_errno() messages\n\nBased on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\nand https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated\nto work with the current codebase and address all review feedback.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n    fsmonitor: implement filesystem change listener for Linux\n    \n    Implement fsmonitor for Linux using the inotify API, bringing it to\n    feature parity with existing Windows and macOS implementations.\n    \n    The Linux implementation uses inotify to monitor filesystem events.\n    Unlike macOS's FSEvents which can watch a single root directory, inotify\n    requires registering watches on every directory of interest. The\n    implementation carefully handles directory renames and moves using\n    inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO event\n    pairs.\n    \n    Key implementation details:\n    \n     * Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n     * Maintains bidirectional hashmaps between watch descriptors and paths\n       for efficient event processing\n     * Handles directory creation, deletion, and renames dynamically\n     * Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n     * Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n    \n    Build configuration:\n    \n     * Enabled via FSMONITOR_DAEMON_BACKEND=linux and\n       FSMONITOR_OS_SETTINGS=linux\n     * Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n     * Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n    \n    Documentation updated to note that fsmonitor.socketDir is now supported\n    on both Mac OS and Linux, and adds a section about inotify watch limits.\n    \n    Testing performed:\n    \n     * Build succeeds with standard flags (NO_CURL, NO_GETTEXT, NO_TCLTK)\n     * Build succeeds with SANITIZE=address (AddressSanitizer)\n     * General tests pass (t0001-init, t3200-branch, etc.)\n     * fsmonitor tests require local filesystem (v9fs test environment is\n       correctly detected as remote and rejected)\n     * Manual testing confirms daemon starts and monitors filesystem when\n       configured with fsmonitor.allowremote=true and fsmonitor.socketdir\n       pointing to a local filesystem (tmpfs)\n    \n    Issues addressed from PR #1352 (git/git) review comments:\n    \n     * GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n       scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     * Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n     * Unsafe hashmap iteration in dtor: Collect entries first, then modify\n     * Missing null checks in stop_async: Added proper guard conditions\n     * dirname() modifying argument: Create copy with xstrdup() first\n     * Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n     * Missing worktree null check: Added BUG() for null worktree\n     * Header updates: Use git-compat-util.h, hash_to_hex_algop()\n     * Code style: Use xstrdup() not xmemdupz(), proper pointer style\n    \n    Issues addressed from PR #1667 (git/git) review comments:\n    \n     * EINTR handling: read() now handles both EAGAIN and EINTR\n     * Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n     * Unchecked add_watch return: Now logs failure in rename_dir()\n     * String building: Consolidated strbuf operations with strbuf_addf()\n     * Translation markers: Added _() to all error_errno() messages\n    \n    Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n    work with the current codebase and address all review feedback.\n    \n    Changes since v1:\n    \n     * Fixed memory leak in builtin/fsmonitor--daemon.c: Added\n       hashmap_clear(&state.cookies) in the cleanup section of\n       fsmonitor_run_daemon() to free the cookies hashmap that was\n       initialized but never released. This fixes the 512-byte leak detected\n       by LeakSanitizer.\n     * Removed 9p (Plan 9) from remote filesystem list in\n       compat/fsmonitor/fsm-path-utils-linux.c: Removed V9FS_MAGIC from\n       is_remote_fs() because inotify works correctly on 9p filesystems.\n       This was incorrectly preventing fsmonitor from running in 9p-based\n       container environments where inotify is fully functional.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v2\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v1:\n\n 1:  aa606458a6 ! 1:  7a7fe25a06 fsmonitor: implement filesystem change listener for Linux\n     @@ Documentation/git-fsmonitor--daemon.adoc: By default, the socket is created in t\n       -------------\n       \n      \n     + ## builtin/fsmonitor--daemon.c ##\n     +@@ builtin/fsmonitor--daemon.c: static int fsmonitor_run_daemon(void)\n     + done:\n     + \tpthread_cond_destroy(&state.cookies_cond);\n     + \tpthread_mutex_destroy(&state.main_lock);\n     ++\thashmap_clear(&state.cookies);\n     + \tfsm_listen__dtor(&state);\n     + \tfsm_health__dtor(&state);\n     + \n     +\n       ## compat/fsmonitor/fsm-health-linux.c (new) ##\n      @@\n      +#include \"git-compat-util.h\"\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +\tcase NFS_SUPER_MAGIC:\n      +\tcase AFS_SUPER_MAGIC:\n      +\tcase CODA_SUPER_MAGIC:\n     -+\tcase V9FS_MAGIC:\n      +\tcase FUSE_SUPER_MAGIC:\n      +\t\treturn 1;\n      +\tdefault:\n\n\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n builtin/fsmonitor--daemon.c                 |   1 +\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-ipc-linux.c            |  61 ++\n compat/fsmonitor/fsm-listen-linux.c         | 738 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 223 ++++++\n compat/fsmonitor/fsm-settings-linux.c       |  71 ++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |  10 +\n 10 files changed, 1173 insertions(+), 6 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-ipc-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n create mode 100644 compat/fsmonitor/fsm-settings-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..92c49f6ab2 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1405,6 +1405,7 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\thashmap_clear(&state.cookies);\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-ipc-linux.c b/compat/fsmonitor/fsm-ipc-linux.c\nnew file mode 100644\nindex 0000000000..d34a6419bc\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-ipc-linux.c\n@@ -0,0 +1,61 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"path.h\"\n+#include \"repository.h\"\n+#include \"strbuf.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, \"fsmonitor--daemon.ipc\")\n+\n+const char *fsmonitor_ipc__get_path(struct repository *r)\n+{\n+\tstatic const char *ipc_path = NULL;\n+\tgit_SHA_CTX sha1ctx;\n+\tchar *sock_dir = NULL;\n+\tstruct strbuf ipc_file = STRBUF_INIT;\n+\tunsigned char hash[GIT_SHA1_RAWSZ];\n+\n+\tif (!r)\n+\t\tBUG(\"No repository passed into fsmonitor_ipc__get_path\");\n+\n+\tif (ipc_path)\n+\t\treturn ipc_path;\n+\n+\t/* By default the socket file is created in the .git directory */\n+\tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n+\t\tipc_path = fsmonitor_ipc__get_default_path();\n+\t\treturn ipc_path;\n+\t}\n+\n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n+\tgit_SHA1_Init(&sha1ctx);\n+\tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n+\tgit_SHA1_Final(hash, &sha1ctx);\n+\n+\trepo_config_get_string(r, \"fsmonitor.socketdir\", &sock_dir);\n+\n+\t/* Create the socket file in either socketDir or $HOME */\n+\tif (sock_dir && *sock_dir) {\n+\t\tstrbuf_addf(&ipc_file, \"%s/.git-fsmonitor-%s\",\n+\t\t\t    sock_dir, hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t} else {\n+\t\tstrbuf_addf(&ipc_file, \"~/.git-fsmonitor-%s\",\n+\t\t\t    hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t}\n+\tfree(sock_dir);\n+\n+\tipc_path = interpolate_path(ipc_file.buf, 1);\n+\tif (!ipc_path)\n+\t\tdie(_(\"Invalid path: %s\"), ipc_file.buf);\n+\n+\tstrbuf_release(&ipc_file);\n+\treturn ipc_path;\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..2593c834d3\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,738 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <poll.h>\n+#include <sys/inotify.h>\n+#include <sys/stat.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"Removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"Double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"No matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"No matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\tconst int interval = 1000;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 1);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"Missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..fc4acbc20d\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,223 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <errno.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/compat/fsmonitor/fsm-settings-linux.c b/compat/fsmonitor/fsm-settings-linux.c\nnew file mode 100644\nindex 0000000000..23e7442d0c\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-settings-linux.c\n@@ -0,0 +1,71 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-settings.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+#include <libgen.h>\n+\n+/*\n+ * For the builtin FSMonitor, we create the Unix domain socket for the\n+ * IPC in the .git directory.  If the working directory is remote,\n+ * then the socket will be created on the remote file system.  This\n+ * can fail if the remote file system does not support UDS file types\n+ * (e.g. smbfs to a Windows server) or if the remote kernel does not\n+ * allow a non-local process to bind() the socket.  (These problems\n+ * could be fixed by moving the UDS out of the .git directory and to a\n+ * well-known local directory on the client machine, but care should\n+ * be taken to ensure that $HOME is actually local and not a managed\n+ * file share.)\n+ *\n+ * FAT32 and NTFS working directories are problematic too.\n+ *\n+ * The builtin FSMonitor uses a Unix domain socket in the .git\n+ * directory for IPC.  These Windows drive formats do not support\n+ * Unix domain sockets, so mark them as incompatible for the daemon.\n+ */\n+static enum fsmonitor_reason check_uds_volume(struct repository *r)\n+{\n+\tstruct fs_info fs;\n+\tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n+\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n+\t\treturn FSMONITOR_REASON_ERROR;\n+\t}\n+\n+\tfree(path);\n+\n+\tif (fs.is_remote ||\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n+\t\tfree(fs.typename);\n+\t\treturn FSMONITOR_REASON_NOSOCKETS;\n+\t}\n+\n+\tfree(fs.typename);\n+\treturn FSMONITOR_REASON_OK;\n+}\n+\n+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)\n+{\n+\tenum fsmonitor_reason reason;\n+\n+\tif (ipc) {\n+\t\treason = check_uds_volume(r);\n+\t\tif (reason != FSMONITOR_REASON_OK)\n+\t\t\treturn reason;\n+\t}\n+\n+\treturn FSMONITOR_REASON_OK;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..a0fd0752a3 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = linux\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..141b05acb0 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -308,6 +308,16 @@ if(SUPPORTS_SIMPLE_IPC)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n+\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)\n \tendif()\n endif()\n \n\nbase-commit: 7c7698a654a7a0031f65b0ab0c1c4e438e95df60\n-- \ngitgitgadget\n"},{"id":"532828","messageId":"pull.2147.v3.git.git.1767099302592.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v2.git.git.1767096494372.gitgitgadget@gmail.com","subject":"[PATCH v3] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T12:55:02Z","receivedAt":"2025-12-30T12:55:05Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement fsmonitor for Linux using the inotify API, bringing it to\nfeature parity with existing Windows and macOS implementations.\n\nThe Linux implementation uses inotify to monitor filesystem events.\nUnlike macOS's FSEvents which can watch a single root directory,\ninotify requires registering watches on every directory of interest.\nThe implementation carefully handles directory renames and moves\nusing inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\nevent pairs.\n\nKey implementation details:\n- Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n- Maintains bidirectional hashmaps between watch descriptors and paths\n  for efficient event processing\n- Handles directory creation, deletion, and renames dynamically\n- Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n- Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n- Creates batches lazily (only for actual file events, not cookies)\n  to avoid spurious sequence number increments\n\nBuild configuration:\n- Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n- Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n- Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nDocumentation updated to note that fsmonitor.socketDir is now supported\non both Mac OS and Linux, and adds a section about inotify watch limits.\n\nTesting performed:\n- Build succeeds with standard flags and SANITIZE=address\n- All t7527-builtin-fsmonitor.sh tests pass on local filesystems\n- Remote filesystem detection correctly rejects network mounts\n\nIssues addressed from PR #1352 (git/git) review comments:\n- GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n  scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n- Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n- Unsafe hashmap iteration in dtor: Collect entries first, then modify\n- Missing null checks in stop_async: Added proper guard conditions\n- dirname() modifying argument: Create copy with xstrdup() first\n- Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n- Missing worktree null check: Added BUG() for null worktree\n- Header updates: Use git-compat-util.h, hash_to_hex_algop()\n- Code style: Use xstrdup() not xmemdupz(), proper pointer style\n\nIssues addressed from PR #1667 (git/git) review comments:\n- EINTR handling: read() now handles both EAGAIN and EINTR\n- Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n- Unchecked add_watch return: Now logs failure in rename_dir()\n- String building: Consolidated strbuf operations with strbuf_addf()\n- Translation markers: Added _() to all error_errno() messages\n\nBased on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\nand https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated\nto work with the current codebase and address all review feedback.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n    fsmonitor: implement filesystem change listener for Linux\n    \n    Implement fsmonitor for Linux using the inotify API, bringing it to\n    feature parity with existing Windows and macOS implementations.\n    \n    The Linux implementation uses inotify to monitor filesystem events.\n    Unlike macOS's FSEvents which can watch a single root directory, inotify\n    requires registering watches on every directory of interest. The\n    implementation carefully handles directory renames and moves using\n    inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO event\n    pairs.\n    \n    Key implementation details:\n    \n     * Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n     * Maintains bidirectional hashmaps between watch descriptors and paths\n       for efficient event processing\n     * Handles directory creation, deletion, and renames dynamically\n     * Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n     * Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n    \n    Build configuration:\n    \n     * Enabled via FSMONITOR_DAEMON_BACKEND=linux and\n       FSMONITOR_OS_SETTINGS=linux\n     * Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n     * Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n    \n    Documentation updated to note that fsmonitor.socketDir is now supported\n    on both Mac OS and Linux, and adds a section about inotify watch limits.\n    \n    Testing performed:\n    \n     * Build succeeds with standard flags (NO_CURL, NO_GETTEXT, NO_TCLTK)\n     * Build succeeds with SANITIZE=address (AddressSanitizer)\n     * General tests pass (t0001-init, t3200-branch, etc.)\n     * fsmonitor tests require local filesystem (v9fs test environment is\n       correctly detected as remote and rejected)\n     * Manual testing confirms daemon starts and monitors filesystem when\n       configured with fsmonitor.allowremote=true and fsmonitor.socketdir\n       pointing to a local filesystem (tmpfs)\n    \n    Issues addressed from PR #1352 (git/git) review comments:\n    \n     * GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n       scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     * Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n     * Unsafe hashmap iteration in dtor: Collect entries first, then modify\n     * Missing null checks in stop_async: Added proper guard conditions\n     * dirname() modifying argument: Create copy with xstrdup() first\n     * Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n     * Missing worktree null check: Added BUG() for null worktree\n     * Header updates: Use git-compat-util.h, hash_to_hex_algop()\n     * Code style: Use xstrdup() not xmemdupz(), proper pointer style\n    \n    Issues addressed from PR #1667 (git/git) review comments:\n    \n     * EINTR handling: read() now handles both EAGAIN and EINTR\n     * Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n     * Unchecked add_watch return: Now logs failure in rename_dir()\n     * String building: Consolidated strbuf operations with strbuf_addf()\n     * Translation markers: Added _() to all error_errno() messages\n    \n    Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n    work with the current codebase and address all review feedback.\n    \n    Changes since v2:\n    \n     * Included fix khash memory leak in do_handle_client\n       https://github.com/git/git/pull/2148\n    \n    Changes since v1:\n    \n     * Fixed memory leak in builtin/fsmonitor--daemon.c: Added\n       hashmap_clear(&state.cookies) in the cleanup section of\n       fsmonitor_run_daemon() to free the cookies hashmap that was\n       initialized but never released. This fixes the 512-byte leak detected\n       by LeakSanitizer.\n     * Removed 9p (Plan 9) from remote filesystem list in\n       compat/fsmonitor/fsm-path-utils-linux.c: Removed V9FS_MAGIC from\n       is_remote_fs() because inotify works correctly on 9p filesystems.\n       This was incorrectly preventing fsmonitor from running in 9p-based\n       container environments where inotify is fully functional.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v3\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v2:\n\n 1:  7a7fe25a06 ! 1:  207da682f4 fsmonitor: implement filesystem change listener for Linux\n     @@ Documentation/git-fsmonitor--daemon.adoc: By default, the socket is created in t\n       \n      \n       ## builtin/fsmonitor--daemon.c ##\n     +@@ builtin/fsmonitor--daemon.c: static int do_handle_client(struct fsmonitor_daemon_state *state,\n     + \tconst struct fsmonitor_batch *batch;\n     + \tstruct fsmonitor_batch *remainder = NULL;\n     + \tintmax_t count = 0, duplicates = 0;\n     +-\tkh_str_t *shown;\n     ++\tkh_str_t *shown = NULL;\n     + \tint hash_ret;\n     + \tint do_trivial = 0;\n     + \tint do_flush = 0;\n     +@@ builtin/fsmonitor--daemon.c: static int do_handle_client(struct fsmonitor_daemon_state *state,\n     + \t\ttotal_response_len += payload.len;\n     + \t}\n     + \n     +-\tkh_release_str(shown);\n     +-\n     + \tpthread_mutex_lock(&state->main_lock);\n     + \n     + \tif (token_data->client_ref_count > 0)\n     +@@ builtin/fsmonitor--daemon.c: static int do_handle_client(struct fsmonitor_daemon_state *state,\n     + \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n     + \n     + cleanup:\n     ++\tkh_destroy_str(shown);\n     + \tstrbuf_release(&response_token);\n     + \tstrbuf_release(&requested_token_id);\n     + \tstrbuf_release(&payload);\n      @@ builtin/fsmonitor--daemon.c: static int fsmonitor_run_daemon(void)\n       done:\n       \tpthread_cond_destroy(&state.cookies_cond);\n\n\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n builtin/fsmonitor--daemon.c                 |   6 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-ipc-linux.c            |  61 ++\n compat/fsmonitor/fsm-listen-linux.c         | 738 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 223 ++++++\n compat/fsmonitor/fsm-settings-linux.c       |  71 ++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |  10 +\n 10 files changed, 1175 insertions(+), 9 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-ipc-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n create mode 100644 compat/fsmonitor/fsm-settings-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..4d52622e24 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n@@ -1405,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\thashmap_clear(&state.cookies);\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-ipc-linux.c b/compat/fsmonitor/fsm-ipc-linux.c\nnew file mode 100644\nindex 0000000000..d34a6419bc\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-ipc-linux.c\n@@ -0,0 +1,61 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"path.h\"\n+#include \"repository.h\"\n+#include \"strbuf.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, \"fsmonitor--daemon.ipc\")\n+\n+const char *fsmonitor_ipc__get_path(struct repository *r)\n+{\n+\tstatic const char *ipc_path = NULL;\n+\tgit_SHA_CTX sha1ctx;\n+\tchar *sock_dir = NULL;\n+\tstruct strbuf ipc_file = STRBUF_INIT;\n+\tunsigned char hash[GIT_SHA1_RAWSZ];\n+\n+\tif (!r)\n+\t\tBUG(\"No repository passed into fsmonitor_ipc__get_path\");\n+\n+\tif (ipc_path)\n+\t\treturn ipc_path;\n+\n+\t/* By default the socket file is created in the .git directory */\n+\tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n+\t\tipc_path = fsmonitor_ipc__get_default_path();\n+\t\treturn ipc_path;\n+\t}\n+\n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n+\tgit_SHA1_Init(&sha1ctx);\n+\tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n+\tgit_SHA1_Final(hash, &sha1ctx);\n+\n+\trepo_config_get_string(r, \"fsmonitor.socketdir\", &sock_dir);\n+\n+\t/* Create the socket file in either socketDir or $HOME */\n+\tif (sock_dir && *sock_dir) {\n+\t\tstrbuf_addf(&ipc_file, \"%s/.git-fsmonitor-%s\",\n+\t\t\t    sock_dir, hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t} else {\n+\t\tstrbuf_addf(&ipc_file, \"~/.git-fsmonitor-%s\",\n+\t\t\t    hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t}\n+\tfree(sock_dir);\n+\n+\tipc_path = interpolate_path(ipc_file.buf, 1);\n+\tif (!ipc_path)\n+\t\tdie(_(\"Invalid path: %s\"), ipc_file.buf);\n+\n+\tstrbuf_release(&ipc_file);\n+\treturn ipc_path;\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..2593c834d3\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,738 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <poll.h>\n+#include <sys/inotify.h>\n+#include <sys/stat.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"Removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"Double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"No matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"No matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\tconst int interval = 1000;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 1);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"Missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..fc4acbc20d\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,223 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <errno.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/compat/fsmonitor/fsm-settings-linux.c b/compat/fsmonitor/fsm-settings-linux.c\nnew file mode 100644\nindex 0000000000..23e7442d0c\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-settings-linux.c\n@@ -0,0 +1,71 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-settings.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+#include <libgen.h>\n+\n+/*\n+ * For the builtin FSMonitor, we create the Unix domain socket for the\n+ * IPC in the .git directory.  If the working directory is remote,\n+ * then the socket will be created on the remote file system.  This\n+ * can fail if the remote file system does not support UDS file types\n+ * (e.g. smbfs to a Windows server) or if the remote kernel does not\n+ * allow a non-local process to bind() the socket.  (These problems\n+ * could be fixed by moving the UDS out of the .git directory and to a\n+ * well-known local directory on the client machine, but care should\n+ * be taken to ensure that $HOME is actually local and not a managed\n+ * file share.)\n+ *\n+ * FAT32 and NTFS working directories are problematic too.\n+ *\n+ * The builtin FSMonitor uses a Unix domain socket in the .git\n+ * directory for IPC.  These Windows drive formats do not support\n+ * Unix domain sockets, so mark them as incompatible for the daemon.\n+ */\n+static enum fsmonitor_reason check_uds_volume(struct repository *r)\n+{\n+\tstruct fs_info fs;\n+\tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n+\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n+\t\treturn FSMONITOR_REASON_ERROR;\n+\t}\n+\n+\tfree(path);\n+\n+\tif (fs.is_remote ||\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n+\t\tfree(fs.typename);\n+\t\treturn FSMONITOR_REASON_NOSOCKETS;\n+\t}\n+\n+\tfree(fs.typename);\n+\treturn FSMONITOR_REASON_OK;\n+}\n+\n+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)\n+{\n+\tenum fsmonitor_reason reason;\n+\n+\tif (ipc) {\n+\t\treason = check_uds_volume(r);\n+\t\tif (reason != FSMONITOR_REASON_OK)\n+\t\t\treturn reason;\n+\t}\n+\n+\treturn FSMONITOR_REASON_OK;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..a0fd0752a3 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = linux\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..141b05acb0 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -308,6 +308,16 @@ if(SUPPORTS_SIMPLE_IPC)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n+\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)\n \tendif()\n endif()\n \n\nbase-commit: 7c7698a654a7a0031f65b0ab0c1c4e438e95df60\n-- \ngitgitgadget\n"},{"id":"532842","messageId":"xmqq5x9of0ae.fsf@gitster.g","threadId":"64696","inReplyTo":"pull.2147.v2.git.git.1767096494372.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-30T15:37:45Z","receivedAt":"2025-12-30T15:37:48Z","isPatch":true,"body":"Thanks for a quick turnaround, but it would be more efficient if you\nhunted all the leaks yourself, instead of getting a report for one\nissue and updating the patch to fix that one issue.\n\nHere is what I am getting these:\n\n    $ make SANITIZE=address CC=clang && cd t && sh t7527-*.sh -i -v\n\nNote that \"-i\" is to say \"stop at the first one\".\n\n\nexpecting success of 7527.12 'create some files':\n        test_when_finished clean_up_repo_and_stop_daemon &&\n\n        start_daemon --tf \"$PWD/.git/trace\" &&\n\n        create_files &&\n\n        test-tool fsmonitor-client query --token 0 &&\n\n        grep \"^event: dir1/new$\" .git/trace &&\n        grep \"^event: dir2/new$\" .git/trace &&\n        grep \"^event: new$\"      .git/trace\n\nfsmonitor-daemon is watching '/home/gitster/w/git.git/t/trash directory.t7527-builtin-fsmonitor'\nbuiltin:0.1039108.20251230T123036.129805Z:0/event: dir1/new\nevent: dir1/new\nevent: dir2/new\nevent: dir2/new\nevent: new\nevent: new\nHEAD is now at 1d1edcb initial\nRemoving dir1/new\nRemoving dir2/new\nRemoving new\nnot ok 12 - create some files\n#\n#               test_when_finished clean_up_repo_and_stop_daemon &&\n#\n#               start_daemon --tf \"$PWD/.git/trace\" &&\n#\n#               create_files &&\n#\n#               test-tool fsmonitor-client query --token 0 &&\n#\n#               grep \"^event: dir1/new$\" .git/trace &&\n#               grep \"^event: dir2/new$\" .git/trace &&\n#               grep \"^event: new$\"      .git/trace\n#\n1..12\n\n=================================================================\n==git==1039073==ERROR: LeakSanitizer: detected memory leaks\n\nDirect leak of 40 byte(s) in 1 object(s) allocated from:\n    #0 0x55c18d8d4042 in calloc (git+0x8c042) (BuildId: 4097db008a82663ae0b3398128a7ab4e09bbdd21)\n    #1 0x55c18dc10f14 in xcalloc wrapper.c:154:8\n    #2 0x55c18d945f72 in kh_init_str builtin/fsmonitor--daemon.c:656:1\n    #3 0x55c18d945828 in do_handle_client builtin/fsmonitor--daemon.c:871:10\n    #4 0x55c18d945191 in handle_client builtin/fsmonitor--daemon.c:987:11\n    #5 0x55c18dc283e2 in worker_thread__do_io compat/simple-ipc/ipc-unix-socket.c:532:9\n    #6 0x55c18dc27a7f in worker_thread_proc compat/simple-ipc/ipc-unix-socket.c:606:9\n    #7 0x55c18d8d64f4 in void* ThreadStartFunc<false>(void*) lsan_interceptors.cpp.o\n    #8 0x7fe358257b7a in start_thread nptl/pthread_create.c:448:8\n    #9 0x7fe3582d57b7 in __GI___clone3 misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:78\n"},{"id":"532866","messageId":"pull.2147.v4.git.git.1767202894884.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v3.git.git.1767099302592.gitgitgadget@gmail.com","subject":"[PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-31T17:41:34Z","receivedAt":"2025-12-31T17:41:39Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement fsmonitor for Linux using the inotify API, bringing it to\nfeature parity with existing Windows and macOS implementations.\n\nThe Linux implementation uses inotify to monitor filesystem events.\nUnlike macOS's FSEvents which can watch a single root directory,\ninotify requires registering watches on every directory of interest.\nThe implementation carefully handles directory renames and moves\nusing inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\nevent pairs.\n\nKey implementation details:\n- Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n- Maintains bidirectional hashmaps between watch descriptors and paths\n  for efficient event processing\n- Handles directory creation, deletion, and renames dynamically\n- Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n- Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n- Creates batches lazily (only for actual file events, not cookies)\n  to avoid spurious sequence number increments\n\nBuild configuration:\n- Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n- Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n- Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nDocumentation updated to note that fsmonitor.socketDir is now supported\non both Mac OS and Linux, and adds a section about inotify watch limits.\n\nTesting performed:\n- Build succeeds with standard flags and SANITIZE=address\n- All t7527-builtin-fsmonitor.sh tests pass on local filesystems\n- Remote filesystem detection correctly rejects network mounts\n\nIssues addressed from PR #1352 (git/git) review comments:\n- GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n  scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n- Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n- Unsafe hashmap iteration in dtor: Collect entries first, then modify\n- Missing null checks in stop_async: Added proper guard conditions\n- dirname() modifying argument: Create copy with xstrdup() first\n- Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n- Missing worktree null check: Added BUG() for null worktree\n- Header updates: Use git-compat-util.h, hash_to_hex_algop()\n- Code style: Use xstrdup() not xmemdupz(), proper pointer style\n\nIssues addressed from PR #1667 (git/git) review comments:\n- EINTR handling: read() now handles both EAGAIN and EINTR\n- Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n- Unchecked add_watch return: Now logs failure in rename_dir()\n- String building: Consolidated strbuf operations with strbuf_addf()\n- Translation markers: Added _() to all error_errno() messages\n\nBased on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\nand https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated\nto work with the current codebase and address all review feedback.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n    fsmonitor: implement filesystem change listener for Linux\n    \n    Implement fsmonitor for Linux using the inotify API, bringing it to\n    feature parity with existing Windows and macOS implementations.\n    \n    The Linux implementation uses inotify to monitor filesystem events.\n    Unlike macOS's FSEvents which can watch a single root directory, inotify\n    requires registering watches on every directory of interest. The\n    implementation carefully handles directory renames and moves using\n    inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO event\n    pairs.\n    \n    Key implementation details:\n    \n     * Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n     * Maintains bidirectional hashmaps between watch descriptors and paths\n       for efficient event processing\n     * Handles directory creation, deletion, and renames dynamically\n     * Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n     * Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n    \n    Build configuration:\n    \n     * Enabled via FSMONITOR_DAEMON_BACKEND=linux and\n       FSMONITOR_OS_SETTINGS=linux\n     * Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n     * Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n    \n    Documentation updated to note that fsmonitor.socketDir is now supported\n    on both Mac OS and Linux, and adds a section about inotify watch limits.\n    \n    Testing performed:\n    \n     * Build succeeds with standard flags\n     * Build succeeds with SANITIZE=address (AddressSanitizer)\n     * General tests pass (t0001-init, t3200-branch, etc.)\n    \n    Stress testing on Linux (tmpfs):\n    \n     * Parallel file creation: 50,000 files created in parallel batches, all\n       detected correctly\n     * Rapid file deletion: Mass deletion of 10,000+ files tracked properly\n     * Deep nesting: 100-level deep directory hierarchies handled correctly\n     * Directory renames: 2,000+ rename operations including rapid rename\n       chains\n     * Directory move in/out: Directories moved from outside to inside watch\n       tree and back\n     * Concurrent git operations: 100 parallel git status commands while\n       filesystem changes occur\n     * Burst events: 60,000+ rapid-fire inotify events (exceeding default\n       queue size of 16,384)\n     * Race conditions: Rapid mkdir+create+rmdir cycles and rename\n       back-and-forth stress\n     * Sustained load: 60 seconds of continuous high-throughput operations\n       (10 parallel workers), daemon remained stable with 377,000+ tracked\n       files\n     * Git worktrees: 10 worktrees with 500 files across them\n     * Special files: Hardlinks, symlinks, FIFOs (named pipes)\n     * File operations: Permission changes (3,000 chmod), timestamp updates\n       (2,000 touch), truncate operations (3,000 truncates)\n     * Large files: 3x 100MB files created in parallel\n     * Unicode filenames: CJK, Cyrillic, Greek, Arabic, emoji characters\n     * IDE simulation: 1,000 rapid edit/save cycles mimicking editor\n       behavior\n     * Maximum chaos: All operation types running simultaneously\n    \n    Issues addressed from PR #1352 (git/git) review comments:\n    \n     * GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n       scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     * Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n     * Unsafe hashmap iteration in dtor: Collect entries first, then modify\n     * Missing null checks in stop_async: Added proper guard conditions\n     * dirname() modifying argument: Create copy with xstrdup() first\n     * Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n     * Missing worktree null check: Added BUG() for null worktree\n     * Header updates: Use git-compat-util.h, hash_to_hex_algop()\n     * Code style: Use xstrdup() not xmemdupz(), proper pointer style\n    \n    Issues addressed from PR #1667 (git/git) review comments:\n    \n     * EINTR handling: read() now handles both EAGAIN and EINTR\n     * Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n     * Unchecked add_watch return: Now logs failure in rename_dir()\n     * String building: Consolidated strbuf operations with strbuf_addf()\n     * Translation markers: Added _() to all error_errno() messages\n    \n    Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n    work with the current codebase and address all review feedback.\n    \n    Changes since v3:\n    \n     * Fix for rapid creation of nested directories where the daemon could\n       crash with: inotify_add_watch('...') failed: File exists\n     * Did a lot of stress testing\n    \n    Changes since v2:\n    \n     * Included fix khash memory leak in do_handle_client\n       https://github.com/git/git/pull/2148\n    \n    Changes since v1:\n    \n     * Fixed memory leak in builtin/fsmonitor--daemon.c: Added\n       hashmap_clear(&state.cookies) in the cleanup section of\n       fsmonitor_run_daemon() to free the cookies hashmap that was\n       initialized but never released. This fixes the 512-byte leak detected\n       by LeakSanitizer.\n     * Removed 9p (Plan 9) from remote filesystem list in\n       compat/fsmonitor/fsm-path-utils-linux.c: Removed V9FS_MAGIC from\n       is_remote_fs() because inotify works correctly on 9p filesystems.\n       This was incorrectly preventing fsmonitor from running in 9p-based\n       container environments where inotify is fully functional.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v4\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v3:\n\n 1:  207da682f4 ! 1:  c7fd346e89 fsmonitor: implement filesystem change listener for Linux\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\tif (wd < 0) {\n      +\t\tif (errno == ENOENT || errno == ENOTDIR)\n      +\t\t\treturn 0; /* directory was deleted or is not a directory */\n     ++\t\tif (errno == EEXIST)\n     ++\t\t\treturn 0; /* watch already exists, no action needed */\n      +\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n      +\t}\n      +\n     @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n       \tendif()\n       endif()\n       \n     +\n     + ## t/t7527-builtin-fsmonitor.sh ##\n     +@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'directory changes to a file' '\n     + \tgrep \"^event: dir1$\" .git/trace\n     + '\n     + \n     ++test_expect_success 'rapid nested directory creation' '\n     ++\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n     ++\n     ++\tstart_daemon --tf \"$PWD/.git/trace\" &&\n     ++\n     ++\t# Rapidly create nested directories to exercise race conditions\n     ++\t# where directory watches may be added concurrently during\n     ++\t# event processing and recursive scanning.\n     ++\tfor i in $(test_seq 1 20)\n     ++\tdo\n     ++\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n     ++\tdone &&\n     ++\n     ++\t# Give the daemon time to process all events\n     ++\tsleep 1 &&\n     ++\n     ++\ttest-tool fsmonitor-client query --token 0 &&\n     ++\n     ++\t# Verify daemon is still running (did not crash)\n     ++\tgit fsmonitor--daemon status\n     ++'\n     ++\n     + # The next few test cases exercise the token-resync code.  When filesystem\n     + # drops events (because of filesystem velocity or because the daemon isn't\n     + # polling fast enough), we need to discard the cached data (relative to the\n\n\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n builtin/fsmonitor--daemon.c                 |   6 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-ipc-linux.c            |  61 ++\n compat/fsmonitor/fsm-listen-linux.c         | 740 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 223 ++++++\n compat/fsmonitor/fsm-settings-linux.c       |  71 ++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |  10 +\n t/t7527-builtin-fsmonitor.sh                |  22 +\n 11 files changed, 1199 insertions(+), 9 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-ipc-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n create mode 100644 compat/fsmonitor/fsm-settings-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..4d52622e24 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n@@ -1405,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\thashmap_clear(&state.cookies);\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-ipc-linux.c b/compat/fsmonitor/fsm-ipc-linux.c\nnew file mode 100644\nindex 0000000000..d34a6419bc\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-ipc-linux.c\n@@ -0,0 +1,61 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"path.h\"\n+#include \"repository.h\"\n+#include \"strbuf.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, \"fsmonitor--daemon.ipc\")\n+\n+const char *fsmonitor_ipc__get_path(struct repository *r)\n+{\n+\tstatic const char *ipc_path = NULL;\n+\tgit_SHA_CTX sha1ctx;\n+\tchar *sock_dir = NULL;\n+\tstruct strbuf ipc_file = STRBUF_INIT;\n+\tunsigned char hash[GIT_SHA1_RAWSZ];\n+\n+\tif (!r)\n+\t\tBUG(\"No repository passed into fsmonitor_ipc__get_path\");\n+\n+\tif (ipc_path)\n+\t\treturn ipc_path;\n+\n+\t/* By default the socket file is created in the .git directory */\n+\tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n+\t\tipc_path = fsmonitor_ipc__get_default_path();\n+\t\treturn ipc_path;\n+\t}\n+\n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n+\tgit_SHA1_Init(&sha1ctx);\n+\tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n+\tgit_SHA1_Final(hash, &sha1ctx);\n+\n+\trepo_config_get_string(r, \"fsmonitor.socketdir\", &sock_dir);\n+\n+\t/* Create the socket file in either socketDir or $HOME */\n+\tif (sock_dir && *sock_dir) {\n+\t\tstrbuf_addf(&ipc_file, \"%s/.git-fsmonitor-%s\",\n+\t\t\t    sock_dir, hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t} else {\n+\t\tstrbuf_addf(&ipc_file, \"~/.git-fsmonitor-%s\",\n+\t\t\t    hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t}\n+\tfree(sock_dir);\n+\n+\tipc_path = interpolate_path(ipc_file.buf, 1);\n+\tif (!ipc_path)\n+\t\tdie(_(\"Invalid path: %s\"), ipc_file.buf);\n+\n+\tstrbuf_release(&ipc_file);\n+\treturn ipc_path;\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..04441c5120\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,740 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <poll.h>\n+#include <sys/inotify.h>\n+#include <sys/stat.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"Removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"Double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"No matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"No matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\tconst int interval = 1000;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 1);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"Missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..fc4acbc20d\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,223 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <errno.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/compat/fsmonitor/fsm-settings-linux.c b/compat/fsmonitor/fsm-settings-linux.c\nnew file mode 100644\nindex 0000000000..23e7442d0c\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-settings-linux.c\n@@ -0,0 +1,71 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-settings.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+#include <libgen.h>\n+\n+/*\n+ * For the builtin FSMonitor, we create the Unix domain socket for the\n+ * IPC in the .git directory.  If the working directory is remote,\n+ * then the socket will be created on the remote file system.  This\n+ * can fail if the remote file system does not support UDS file types\n+ * (e.g. smbfs to a Windows server) or if the remote kernel does not\n+ * allow a non-local process to bind() the socket.  (These problems\n+ * could be fixed by moving the UDS out of the .git directory and to a\n+ * well-known local directory on the client machine, but care should\n+ * be taken to ensure that $HOME is actually local and not a managed\n+ * file share.)\n+ *\n+ * FAT32 and NTFS working directories are problematic too.\n+ *\n+ * The builtin FSMonitor uses a Unix domain socket in the .git\n+ * directory for IPC.  These Windows drive formats do not support\n+ * Unix domain sockets, so mark them as incompatible for the daemon.\n+ */\n+static enum fsmonitor_reason check_uds_volume(struct repository *r)\n+{\n+\tstruct fs_info fs;\n+\tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n+\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n+\t\treturn FSMONITOR_REASON_ERROR;\n+\t}\n+\n+\tfree(path);\n+\n+\tif (fs.is_remote ||\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n+\t\tfree(fs.typename);\n+\t\treturn FSMONITOR_REASON_NOSOCKETS;\n+\t}\n+\n+\tfree(fs.typename);\n+\treturn FSMONITOR_REASON_OK;\n+}\n+\n+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)\n+{\n+\tenum fsmonitor_reason reason;\n+\n+\tif (ipc) {\n+\t\treason = check_uds_volume(r);\n+\t\tif (reason != FSMONITOR_REASON_OK)\n+\t\t\treturn reason;\n+\t}\n+\n+\treturn FSMONITOR_REASON_OK;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..a0fd0752a3 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = linux\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..141b05acb0 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -308,6 +308,16 @@ if(SUPPORTS_SIMPLE_IPC)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n+\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)\n \tendif()\n endif()\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..d2f1f1097e 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -520,6 +520,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n\nbase-commit: 7c7698a654a7a0031f65b0ab0c1c4e438e95df60\n-- \ngitgitgadget\n"},{"id":"533031","messageId":"aVuplzNaoCHlZG3S@pks.im","threadId":"64696","inReplyTo":"pull.2147.v4.git.git.1767202894884.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-05T12:07:51Z","receivedAt":"2026-01-05T12:07:57Z","isPatch":true,"body":"On Wed, Dec 31, 2025 at 05:41:34PM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> Implement fsmonitor for Linux using the inotify API, bringing it to\n> feature parity with existing Windows and macOS implementations.\n> \n> The Linux implementation uses inotify to monitor filesystem events.\n> Unlike macOS's FSEvents which can watch a single root directory,\n> inotify requires registering watches on every directory of interest.\n> The implementation carefully handles directory renames and moves\n> using inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\n> event pairs.\n> \n> Key implementation details:\n> - Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n> - Maintains bidirectional hashmaps between watch descriptors and paths\n>   for efficient event processing\n> - Handles directory creation, deletion, and renames dynamically\n> - Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n> - Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n> - Creates batches lazily (only for actual file events, not cookies)\n>   to avoid spurious sequence number increments\n> \n> Build configuration:\n> - Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n> - Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n> - Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nThis would also need the below patch to support Meson. Would be great if\nyou include it, otherwise I can send it as a separate patch once this\ntopic lands. Thanks!\n\nPatrick\n\n-- >8 --\n\ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..0130d40702 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1322,6 +1322,9 @@ endif\n fsmonitor_backend = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   libgit_dependencies += dependency('CoreServices')\n"},{"id":"536552","messageId":"xmqqikbrvz2l.fsf@gitster.g","threadId":"64696","inReplyTo":"aVuplzNaoCHlZG3S@pks.im","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-20T22:18:26Z","receivedAt":"2026-02-20T22:18:29Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> This would also need the below patch to support Meson. Would be great if\n> you include it, otherwise I can send it as a separate patch once this\n> topic lands. Thanks!\n>\n> Patrick\n> ...\n\nI just noticed that the discussion thread went silent after this\nmessage.  Has the patch been reviewed and tested well to proceed,\nexcept for that meson-build support?\n\nThanks.\n"},{"id":"536590","messageId":"CALvWuB70kwPAnQ+v4ch1TKMDxbUQgi5NP8NX7tbCZRqivJ=vig@mail.gmail.com","threadId":"64696","inReplyTo":"xmqqikbrvz2l.fsf@gitster.g","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-21T16:15:02Z","receivedAt":"2026-02-21T16:15:16Z","isPatch":true,"body":"> I just noticed that the discussion thread went silent after this\n> message.  Has the patch been reviewed and tested well to proceed,\n> except for that meson-build support?\n\nI'd love to see it integrated upstream. Is there anything else you\nneed from me? I've been using the patch for 2 months at my work over a\nvery large fleet and it has been working great.\n\n> > This would also need the below patch to support Meson. Would be great if\n> > you include it, otherwise I can send it as a separate patch once this\n> > topic lands. Thanks!\n\nI'd prefer to take you up on the offer to send the meson support as a\nseparate patch. I'm unfamiliar with that system and the suggested\npatch failed in CI on some dependency installation steps which felt\nunrelated but I didn't want to debug.\nhttps://github.com/git/git/actions/runs/20720903513\n\nThanks\nPaul\n"},{"id":"536596","messageId":"xmqq1piet47r.fsf@gitster.g","threadId":"64696","inReplyTo":"CALvWuB70kwPAnQ+v4ch1TKMDxbUQgi5NP8NX7tbCZRqivJ=vig@mail.gmail.com","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-21T17:07:52Z","receivedAt":"2026-02-21T17:07:55Z","isPatch":true,"body":"Paul Tarjan <paul@paultarjan.com> writes:\n\n>> I just noticed that the discussion thread went silent after this\n>> message.  Has the patch been reviewed and tested well to proceed,\n>> except for that meson-build support?\n>\n> I'd love to see it integrated upstream. Is there anything else you\n> need from me?\n>\n>> > This would also need the below patch to support Meson. Would be great if\n>> > you include it, otherwise I can send it as a separate patch once this\n>> > topic lands. Thanks!\n>\n> I'd prefer to take you up on the offer to send the meson support as a\n> separate patch.\n\nThis part of your message is one thing we needed from you to unblock\nourselves, I guess.\n\nPatrick, do you think you can help making this into two-patch\nseries, the original one being the [PATCH 1/2] and update for\nmeson-build in [PATCH 2/2]?\n\n> I'm unfamiliar with that system and the suggested\n> patch failed in CI on some dependency installation steps which felt\n> unrelated but I didn't want to debug.\n> https://github.com/git/git/actions/runs/20720903513\n\nThe topic has been in my tree near the tip of 'seen' and I do not\nthink we saw CI failures coming from this topic.\n\nThanks.\n\n"},{"id":"536690","messageId":"aZv02KjfheyFlMfb@pks.im","threadId":"64696","inReplyTo":"xmqq1piet47r.fsf@gitster.g","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-23T06:34:00Z","receivedAt":"2026-02-23T06:34:07Z","isPatch":true,"body":"On Sat, Feb 21, 2026 at 09:07:52AM -0800, Junio C Hamano wrote:\n> Paul Tarjan <paul@paultarjan.com> writes:\n> \n> >> I just noticed that the discussion thread went silent after this\n> >> message.  Has the patch been reviewed and tested well to proceed,\n> >> except for that meson-build support?\n> >\n> > I'd love to see it integrated upstream. Is there anything else you\n> > need from me?\n> >\n> >> > This would also need the below patch to support Meson. Would be great if\n> >> > you include it, otherwise I can send it as a separate patch once this\n> >> > topic lands. Thanks!\n> >\n> > I'd prefer to take you up on the offer to send the meson support as a\n> > separate patch.\n> \n> This part of your message is one thing we needed from you to unblock\n> ourselves, I guess.\n> \n> Patrick, do you think you can help making this into two-patch\n> series, the original one being the [PATCH 1/2] and update for\n> meson-build in [PATCH 2/2]?\n\nThe changes I sent should be sufficient, so I'd propose to just roll\nit into the v5 patch.\n\n> > I'm unfamiliar with that system and the suggested\n> > patch failed in CI on some dependency installation steps which felt\n> > unrelated but I didn't want to debug.\n> > https://github.com/git/git/actions/runs/20720903513\n> \n> The topic has been in my tree near the tip of 'seen' and I do not\n> think we saw CI failures coming from this topic.\n\nYeah, I think this was simply a flaky CI job. The \"linux-reftable\" job\nhas failed installing packages:\n\n  Err:3 http://security.ubuntu.com/ubuntu questing-security/universe amd64 Packages\n    404  Not Found [IP: 91.189.91.83 80]\n    File has unexpected size (89328 != 89310). Mirror sync in progress? [IP: 91.189.91.83 80]\n    Hashes of expected file:\n     - Filesize:89310 [weak]\n     - SHA256:16943889a9abc4aaeb0e701e99db0004ac0241de728183f7a2923bb7927b107b\n     - SHA1:fac9e79fb36b57de3770e639c4b5bf9231342f15 [weak]\n     - MD5Sum:8cf081c59fbb279da27867d94f2b9520 [weak]\n    Release file created at: Mon, 05 Jan 2026 13:30:45 +0000\n\nAnd all the other jobs simply got aborted because of that initial\nfailure. In any case, the changes work alright with Meson on my system.\n\nBy the way, I haven't yet done a full review of this patch, I only\nchimed in to help out with Meson. But I can have a deeper look once v5\nwas sent out.\n\nThanks!\n\nPatrick\n"},{"id":"536814","messageId":"xmqq342rpiuh.fsf@gitster.g","threadId":"64696","inReplyTo":"aZv02KjfheyFlMfb@pks.im","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-23T15:42:14Z","receivedAt":"2026-02-23T15:42:16Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Sat, Feb 21, 2026 at 09:07:52AM -0800, Junio C Hamano wrote:\n>> Paul Tarjan <paul@paultarjan.com> writes:\n>> \n>> > I'd prefer to take you up on the offer to send the meson support as a\n>> > separate patch.\n>> \n>> This part of your message is one thing we needed from you to unblock\n>> ourselves, I guess.\n>> \n>> Patrick, do you think you can help making this into two-patch\n>> series, the original one being the [PATCH 1/2] and update for\n>> meson-build in [PATCH 2/2]?\n>\n> The changes I sent should be sufficient, so I'd propose to just roll\n> it into the v5 patch.\n> ...\n> By the way, I haven't yet done a full review of this patch, I only\n> chimed in to help out with Meson. But I can have a deeper look once v5\n> was sent out.\n\nOK, so it is not quite clear to me who is doing the v5.  Is the\n\"offer to send the meson support as a separate patch\" still valid,\nor we expect Paul to squash in the earlier patch from you to prepare\nthe v5?\n\nThanks.\n"},{"id":"536815","messageId":"aZx2WFMtQUB0jIfM@pks.im","threadId":"64696","inReplyTo":"xmqq342rpiuh.fsf@gitster.g","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-23T15:46:32Z","receivedAt":"2026-02-23T15:46:39Z","isPatch":true,"body":"On Mon, Feb 23, 2026 at 07:42:14AM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > On Sat, Feb 21, 2026 at 09:07:52AM -0800, Junio C Hamano wrote:\n> >> Paul Tarjan <paul@paultarjan.com> writes:\n> >> \n> >> > I'd prefer to take you up on the offer to send the meson support as a\n> >> > separate patch.\n> >> \n> >> This part of your message is one thing we needed from you to unblock\n> >> ourselves, I guess.\n> >> \n> >> Patrick, do you think you can help making this into two-patch\n> >> series, the original one being the [PATCH 1/2] and update for\n> >> meson-build in [PATCH 2/2]?\n> >\n> > The changes I sent should be sufficient, so I'd propose to just roll\n> > it into the v5 patch.\n> > ...\n> > By the way, I haven't yet done a full review of this patch, I only\n> > chimed in to help out with Meson. But I can have a deeper look once v5\n> > was sent out.\n> \n> OK, so it is not quite clear to me who is doing the v5.  Is the\n> \"offer to send the meson support as a separate patch\" still valid,\n> or we expect Paul to squash in the earlier patch from you to prepare\n> the v5?\n\nI'd think the latter, Paul squashes my patch into his commit. I don't\nthink it needs to be a separate patch.\n\nPatrick\n"},{"id":"536898","messageId":"pull.2147.v5.git.git.1771896704209.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v4.git.git.1767202894884.gitgitgadget@gmail.com","subject":"[PATCH v5] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-24T01:31:44Z","receivedAt":"2026-02-24T01:31:47Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement fsmonitor for Linux using the inotify API, bringing it to\nfeature parity with existing Windows and macOS implementations.\n\nThe Linux implementation uses inotify to monitor filesystem events.\nUnlike macOS's FSEvents which can watch a single root directory,\ninotify requires registering watches on every directory of interest.\nThe implementation carefully handles directory renames and moves\nusing inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\nevent pairs.\n\nKey implementation details:\n- Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n- Maintains bidirectional hashmaps between watch descriptors and paths\n  for efficient event processing\n- Handles directory creation, deletion, and renames dynamically\n- Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n- Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n\nBuild configuration:\n- Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n- Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n- Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nDocumentation updated to note that fsmonitor.socketDir is now supported\non both Mac OS and Linux, and adds a section about inotify watch limits.\n\nIssues addressed from PR #1352 (git/git) review comments:\n- GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n  scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n- Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n- Unsafe hashmap iteration in dtor: Collect entries first, then modify\n- Missing null checks in stop_async: Added proper guard conditions\n- dirname() modifying argument: Create copy with xstrdup() first\n- Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n- Missing worktree null check: Added BUG() for null worktree\n- Header updates: Use git-compat-util.h, hash_to_hex_algop()\n- Code style: Use xstrdup() not xmemdupz(), proper pointer style\n\nIssues addressed from PR #1667 (git/git) review comments:\n- EINTR handling: read() now handles both EAGAIN and EINTR\n- Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n- Unchecked add_watch return: Now logs failure in rename_dir()\n- String building: Consolidated strbuf operations with strbuf_addf()\n- Translation markers: Added _() to all error_errno() messages\n\nBased on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\nand https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\nwork with the current codebase and address all review feedback.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n    fsmonitor: implement filesystem change listener for Linux\n    \n    Implement fsmonitor for Linux using the inotify API, bringing it to\n    feature parity with existing Windows and macOS implementations.\n    \n    The Linux implementation uses inotify to monitor filesystem events.\n    Unlike macOS's FSEvents which can watch a single root directory, inotify\n    requires registering watches on every directory of interest. The\n    implementation carefully handles directory renames and moves using\n    inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO event\n    pairs.\n    \n    Key implementation details:\n    \n     * Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n     * Maintains bidirectional hashmaps between watch descriptors and paths\n       for efficient event processing\n     * Handles directory creation, deletion, and renames dynamically\n     * Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n     * Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n    \n    Build configuration:\n    \n     * Enabled via FSMONITOR_DAEMON_BACKEND=linux and\n       FSMONITOR_OS_SETTINGS=linux\n     * Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n     * Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n    \n    Documentation updated to note that fsmonitor.socketDir is now supported\n    on both Mac OS and Linux, and adds a section about inotify watch limits.\n    \n    Testing performed:\n    \n     * Build succeeds with standard flags\n     * Build succeeds with SANITIZE=address (AddressSanitizer)\n     * General tests pass (t0001-init, t3200-branch, etc.)\n    \n    Stress testing on Linux (tmpfs):\n    \n     * Parallel file creation: 50,000 files created in parallel batches, all\n       detected correctly\n     * Rapid file deletion: Mass deletion of 10,000+ files tracked properly\n     * Deep nesting: 100-level deep directory hierarchies handled correctly\n     * Directory renames: 2,000+ rename operations including rapid rename\n       chains\n     * Directory move in/out: Directories moved from outside to inside watch\n       tree and back\n     * Concurrent git operations: 100 parallel git status commands while\n       filesystem changes occur\n     * Burst events: 60,000+ rapid-fire inotify events (exceeding default\n       queue size of 16,384)\n     * Race conditions: Rapid mkdir+create+rmdir cycles and rename\n       back-and-forth stress\n     * Sustained load: 60 seconds of continuous high-throughput operations\n       (10 parallel workers), daemon remained stable with 377,000+ tracked\n       files\n     * Git worktrees: 10 worktrees with 500 files across them\n     * Special files: Hardlinks, symlinks, FIFOs (named pipes)\n     * File operations: Permission changes (3,000 chmod), timestamp updates\n       (2,000 touch), truncate operations (3,000 truncates)\n     * Large files: 3x 100MB files created in parallel\n     * Unicode filenames: CJK, Cyrillic, Greek, Arabic, emoji characters\n     * IDE simulation: 1,000 rapid edit/save cycles mimicking editor\n       behavior\n     * Maximum chaos: All operation types running simultaneously\n    \n    Issues addressed from PR #1352 (git/git) review comments:\n    \n     * GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n       scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     * Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n     * Unsafe hashmap iteration in dtor: Collect entries first, then modify\n     * Missing null checks in stop_async: Added proper guard conditions\n     * dirname() modifying argument: Create copy with xstrdup() first\n     * Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n     * Missing worktree null check: Added BUG() for null worktree\n     * Header updates: Use git-compat-util.h, hash_to_hex_algop()\n     * Code style: Use xstrdup() not xmemdupz(), proper pointer style\n    \n    Issues addressed from PR #1667 (git/git) review comments:\n    \n     * EINTR handling: read() now handles both EAGAIN and EINTR\n     * Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n     * Unchecked add_watch return: Now logs failure in rename_dir()\n     * String building: Consolidated strbuf operations with strbuf_addf()\n     * Translation markers: Added _() to all error_errno() messages\n    \n    Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n    work with the current codebase and address all review feedback.\n    \n    Changes since v3:\n    \n     * Fix for rapid creation of nested directories where the daemon could\n       crash with: inotify_add_watch('...') failed: File exists\n     * Did a lot of stress testing\n    \n    Changes since v2:\n    \n     * Included fix khash memory leak in do_handle_client\n       https://github.com/git/git/pull/2148\n    \n    Changes since v1:\n    \n     * Fixed memory leak in builtin/fsmonitor--daemon.c: Added\n       hashmap_clear(&state.cookies) in the cleanup section of\n       fsmonitor_run_daemon() to free the cookies hashmap that was\n       initialized but never released. This fixes the 512-byte leak detected\n       by LeakSanitizer.\n     * Removed 9p (Plan 9) from remote filesystem list in\n       compat/fsmonitor/fsm-path-utils-linux.c: Removed V9FS_MAGIC from\n       is_remote_fs() because inotify works correctly on 9p filesystems.\n       This was incorrectly preventing fsmonitor from running in 9p-based\n       container environments where inotify is fully functional.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v5\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v4:\n\n 1:  c7fd346e89 ! 1:  c54814eb31 fsmonitor: implement filesystem change listener for Linux\n     @@ Commit message\n          - Handles directory creation, deletion, and renames dynamically\n          - Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n          - Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n     -    - Creates batches lazily (only for actual file events, not cookies)\n     -      to avoid spurious sequence number increments\n      \n          Build configuration:\n          - Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n     @@ Commit message\n          Documentation updated to note that fsmonitor.socketDir is now supported\n          on both Mac OS and Linux, and adds a section about inotify watch limits.\n      \n     -    Testing performed:\n     -    - Build succeeds with standard flags and SANITIZE=address\n     -    - All t7527-builtin-fsmonitor.sh tests pass on local filesystems\n     -    - Remote filesystem detection correctly rejects network mounts\n     -\n          Issues addressed from PR #1352 (git/git) review comments:\n          - GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n            scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     @@ Commit message\n          - Translation markers: Added _() to all error_errno() messages\n      \n          Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n     -    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated\n     -    to work with the current codebase and address all review feedback.\n     +    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n     +    work with the current codebase and address all review feedback.\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n     @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n       endif()\n       \n      \n     + ## meson.build ##\n     +@@ meson.build: endif\n     + fsmonitor_backend = ''\n     + if host_machine.system() == 'windows'\n     +   fsmonitor_backend = 'win32'\n     ++elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n     ++  fsmonitor_backend = 'linux'\n     ++  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n     + elif host_machine.system() == 'darwin'\n     +   fsmonitor_backend = 'darwin'\n     +   libgit_dependencies += dependency('CoreServices')\n     +\n       ## t/t7527-builtin-fsmonitor.sh ##\n      @@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'directory changes to a file' '\n       \tgrep \"^event: dir1$\" .git/trace\n\n\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n builtin/fsmonitor--daemon.c                 |   6 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-ipc-linux.c            |  61 ++\n compat/fsmonitor/fsm-listen-linux.c         | 740 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 223 ++++++\n compat/fsmonitor/fsm-settings-linux.c       |  71 ++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |  10 +\n meson.build                                 |   3 +\n t/t7527-builtin-fsmonitor.sh                |  22 +\n 12 files changed, 1202 insertions(+), 9 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-ipc-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n create mode 100644 compat/fsmonitor/fsm-settings-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..4d52622e24 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n@@ -1405,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\thashmap_clear(&state.cookies);\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-ipc-linux.c b/compat/fsmonitor/fsm-ipc-linux.c\nnew file mode 100644\nindex 0000000000..d34a6419bc\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-ipc-linux.c\n@@ -0,0 +1,61 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"path.h\"\n+#include \"repository.h\"\n+#include \"strbuf.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, \"fsmonitor--daemon.ipc\")\n+\n+const char *fsmonitor_ipc__get_path(struct repository *r)\n+{\n+\tstatic const char *ipc_path = NULL;\n+\tgit_SHA_CTX sha1ctx;\n+\tchar *sock_dir = NULL;\n+\tstruct strbuf ipc_file = STRBUF_INIT;\n+\tunsigned char hash[GIT_SHA1_RAWSZ];\n+\n+\tif (!r)\n+\t\tBUG(\"No repository passed into fsmonitor_ipc__get_path\");\n+\n+\tif (ipc_path)\n+\t\treturn ipc_path;\n+\n+\t/* By default the socket file is created in the .git directory */\n+\tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n+\t\tipc_path = fsmonitor_ipc__get_default_path();\n+\t\treturn ipc_path;\n+\t}\n+\n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n+\tgit_SHA1_Init(&sha1ctx);\n+\tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n+\tgit_SHA1_Final(hash, &sha1ctx);\n+\n+\trepo_config_get_string(r, \"fsmonitor.socketdir\", &sock_dir);\n+\n+\t/* Create the socket file in either socketDir or $HOME */\n+\tif (sock_dir && *sock_dir) {\n+\t\tstrbuf_addf(&ipc_file, \"%s/.git-fsmonitor-%s\",\n+\t\t\t    sock_dir, hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t} else {\n+\t\tstrbuf_addf(&ipc_file, \"~/.git-fsmonitor-%s\",\n+\t\t\t    hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n+\t}\n+\tfree(sock_dir);\n+\n+\tipc_path = interpolate_path(ipc_file.buf, 1);\n+\tif (!ipc_path)\n+\t\tdie(_(\"Invalid path: %s\"), ipc_file.buf);\n+\n+\tstrbuf_release(&ipc_file);\n+\treturn ipc_path;\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..04441c5120\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,740 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <poll.h>\n+#include <sys/inotify.h>\n+#include <sys/stat.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"Removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"Double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"No matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"No matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\tconst int interval = 1000;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 1);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"Missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..fc4acbc20d\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,223 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <errno.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/compat/fsmonitor/fsm-settings-linux.c b/compat/fsmonitor/fsm-settings-linux.c\nnew file mode 100644\nindex 0000000000..23e7442d0c\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-settings-linux.c\n@@ -0,0 +1,71 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-ipc.h\"\n+#include \"fsmonitor-settings.h\"\n+#include \"fsmonitor-path-utils.h\"\n+\n+#include <libgen.h>\n+\n+/*\n+ * For the builtin FSMonitor, we create the Unix domain socket for the\n+ * IPC in the .git directory.  If the working directory is remote,\n+ * then the socket will be created on the remote file system.  This\n+ * can fail if the remote file system does not support UDS file types\n+ * (e.g. smbfs to a Windows server) or if the remote kernel does not\n+ * allow a non-local process to bind() the socket.  (These problems\n+ * could be fixed by moving the UDS out of the .git directory and to a\n+ * well-known local directory on the client machine, but care should\n+ * be taken to ensure that $HOME is actually local and not a managed\n+ * file share.)\n+ *\n+ * FAT32 and NTFS working directories are problematic too.\n+ *\n+ * The builtin FSMonitor uses a Unix domain socket in the .git\n+ * directory for IPC.  These Windows drive formats do not support\n+ * Unix domain sockets, so mark them as incompatible for the daemon.\n+ */\n+static enum fsmonitor_reason check_uds_volume(struct repository *r)\n+{\n+\tstruct fs_info fs;\n+\tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n+\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n+\t\treturn FSMONITOR_REASON_ERROR;\n+\t}\n+\n+\tfree(path);\n+\n+\tif (fs.is_remote ||\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n+\t\tfree(fs.typename);\n+\t\treturn FSMONITOR_REASON_NOSOCKETS;\n+\t}\n+\n+\tfree(fs.typename);\n+\treturn FSMONITOR_REASON_OK;\n+}\n+\n+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)\n+{\n+\tenum fsmonitor_reason reason;\n+\n+\tif (ipc) {\n+\t\treason = check_uds_volume(r);\n+\t\tif (reason != FSMONITOR_REASON_OK)\n+\t\t\treturn reason;\n+\t}\n+\n+\treturn FSMONITOR_REASON_OK;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..a0fd0752a3 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = linux\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..141b05acb0 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -308,6 +308,16 @@ if(SUPPORTS_SIMPLE_IPC)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n+\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..90e2f77908 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1322,6 +1322,9 @@ endif\n fsmonitor_backend = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   libgit_dependencies += dependency('CoreServices')\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..d2f1f1097e 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -520,6 +520,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n\nbase-commit: 7c7698a654a7a0031f65b0ab0c1c4e438e95df60\n-- \ngitgitgadget\n"},{"id":"536899","messageId":"CALvWuB5BnCmcoZgdnqdZiczG+DMQTUByHzJPkNH+H7hBEGma_w@mail.gmail.com","threadId":"64696","inReplyTo":"aZx2WFMtQUB0jIfM@pks.im","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-24T01:34:31Z","receivedAt":"2026-02-24T01:34:43Z","isPatch":true,"body":"On Mon, Feb 23, 2026 at 5:01 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Mon, Feb 23, 2026 at 07:42:14AM -0800, Junio C Hamano wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> >\n> > > On Sat, Feb 21, 2026 at 09:07:52AM -0800, Junio C Hamano wrote:\n> > >> Paul Tarjan <paul@paultarjan.com> writes:\n> > >>\n> > >> > I'd prefer to take you up on the offer to send the meson support as a\n> > >> > separate patch.\n> > >>\n> > >> This part of your message is one thing we needed from you to unblock\n> > >> ourselves, I guess.\n> > >>\n> > >> Patrick, do you think you can help making this into two-patch\n> > >> series, the original one being the [PATCH 1/2] and update for\n> > >> meson-build in [PATCH 2/2]?\n> > >\n> > > The changes I sent should be sufficient, so I'd propose to just roll\n> > > it into the v5 patch.\n> > > ...\n> > > By the way, I haven't yet done a full review of this patch, I only\n> > > chimed in to help out with Meson. But I can have a deeper look once v5\n> > > was sent out.\n> >\n> > OK, so it is not quite clear to me who is doing the v5.  Is the\n> > \"offer to send the meson support as a separate patch\" still valid,\n> > or we expect Paul to squash in the earlier patch from you to prepare\n> > the v5?\n>\n> I'd think the latter, Paul squashes my patch into his commit. I don't\n> think it needs to be a separate patch.\n\nDone. CI still failed in a different way this time:\nhttps://github.com/git/git/actions/runs/22311198802/job/64543526340?pr=2147\n"},{"id":"536931","messageId":"aZ1bOxAW2Y9cssa2@pks.im","threadId":"64696","inReplyTo":"CALvWuB5BnCmcoZgdnqdZiczG+DMQTUByHzJPkNH+H7hBEGma_w@mail.gmail.com","subject":"Re: [PATCH v4] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-24T08:03:07Z","receivedAt":"2026-02-24T08:03:18Z","isPatch":true,"body":"On Mon, Feb 23, 2026 at 06:34:31PM -0700, Paul Tarjan wrote:\n> On Mon, Feb 23, 2026 at 5:01 PM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Mon, Feb 23, 2026 at 07:42:14AM -0800, Junio C Hamano wrote:\n> > > Patrick Steinhardt <ps@pks.im> writes:\n> > >\n> > > > On Sat, Feb 21, 2026 at 09:07:52AM -0800, Junio C Hamano wrote:\n> > > >> Paul Tarjan <paul@paultarjan.com> writes:\n> > > >>\n> > > >> > I'd prefer to take you up on the offer to send the meson support as a\n> > > >> > separate patch.\n> > > >>\n> > > >> This part of your message is one thing we needed from you to unblock\n> > > >> ourselves, I guess.\n> > > >>\n> > > >> Patrick, do you think you can help making this into two-patch\n> > > >> series, the original one being the [PATCH 1/2] and update for\n> > > >> meson-build in [PATCH 2/2]?\n> > > >\n> > > > The changes I sent should be sufficient, so I'd propose to just roll\n> > > > it into the v5 patch.\n> > > > ...\n> > > > By the way, I haven't yet done a full review of this patch, I only\n> > > > chimed in to help out with Meson. But I can have a deeper look once v5\n> > > > was sent out.\n> > >\n> > > OK, so it is not quite clear to me who is doing the v5.  Is the\n> > > \"offer to send the meson support as a separate patch\" still valid,\n> > > or we expect Paul to squash in the earlier patch from you to prepare\n> > > the v5?\n> >\n> > I'd think the latter, Paul squashes my patch into his commit. I don't\n> > think it needs to be a separate patch.\n> \n> Done. CI still failed in a different way this time:\n> https://github.com/git/git/actions/runs/22311198802/job/64543526340?pr=2147\n\nThis looks like executing tests got stuck. Makes me wonder whether there\nis maybe a race condition in the new fsmonitor implementation that\ncauses tests to not progress anymore. Let me have a deeper look at the\ncode.\n\nPatrick\n"},{"id":"536932","messageId":"aZ1bRQv9Cjke298f@pks.im","threadId":"64696","inReplyTo":"pull.2147.v5.git.git.1771896704209.gitgitgadget@gmail.com","subject":"Re: [PATCH v5] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-24T08:03:17Z","receivedAt":"2026-02-24T08:03:22Z","isPatch":true,"body":"On Tue, Feb 24, 2026 at 01:31:44AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> Implement fsmonitor for Linux using the inotify API, bringing it to\n> feature parity with existing Windows and macOS implementations.\n> \n> The Linux implementation uses inotify to monitor filesystem events.\n> Unlike macOS's FSEvents which can watch a single root directory,\n> inotify requires registering watches on every directory of interest.\n> The implementation carefully handles directory renames and moves\n> using inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\n> event pairs.\n\nI think other than stating that this uses inotify, what this commit\nmessage should also explain is why it was chosen over its alternatives\nlike fanotify.\n\n> Key implementation details:\n> - Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n> - Maintains bidirectional hashmaps between watch descriptors and paths\n>   for efficient event processing\n> - Handles directory creation, deletion, and renames dynamically\n> - Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n> - Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n> \n> Build configuration:\n> - Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n> - Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n> - Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n\nIt would be great to avoid all these bulleted lists here and instead\nhave some paragraphs that explain the design decisions.\n\n> Documentation updated to note that fsmonitor.socketDir is now supported\n> on both Mac OS and Linux, and adds a section about inotify watch limits.\n> \n> Issues addressed from PR #1352 (git/git) review comments:\n> - GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n>   scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n> - Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n> - Unsafe hashmap iteration in dtor: Collect entries first, then modify\n> - Missing null checks in stop_async: Added proper guard conditions\n> - dirname() modifying argument: Create copy with xstrdup() first\n> - Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n> - Missing worktree null check: Added BUG() for null worktree\n> - Header updates: Use git-compat-util.h, hash_to_hex_algop()\n> - Code style: Use xstrdup() not xmemdupz(), proper pointer style\n> \n> Issues addressed from PR #1667 (git/git) review comments:\n> - EINTR handling: read() now handles both EAGAIN and EINTR\n> - Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n> - Unchecked add_watch return: Now logs failure in rename_dir()\n> - String building: Consolidated strbuf operations with strbuf_addf()\n> - Translation markers: Added _() to all error_errno() messages\n\nThe issues that you've addressed don't typically belong into the commit\nmessage. They are useful context when part of a cover letter, but are\nless useful in the commit messages themselves.\n\n> Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n> and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n> work with the current codebase and address all review feedback.\n\nIn that case it might make sense to say \"Based-on-patch-by:\" for both of\nthese authors.\n\n> diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\n> index 242c594646..4d52622e24 100644\n> --- a/builtin/fsmonitor--daemon.c\n> +++ b/builtin/fsmonitor--daemon.c\n> @@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \tconst struct fsmonitor_batch *batch;\n>  \tstruct fsmonitor_batch *remainder = NULL;\n>  \tintmax_t count = 0, duplicates = 0;\n> -\tkh_str_t *shown;\n> +\tkh_str_t *shown = NULL;\n>  \tint hash_ret;\n>  \tint do_trivial = 0;\n>  \tint do_flush = 0;\n> @@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \t\ttotal_response_len += payload.len;\n>  \t}\n>  \n> -\tkh_release_str(shown);\n> -\n>  \tpthread_mutex_lock(&state->main_lock);\n>  \n>  \tif (token_data->client_ref_count > 0)\n> @@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n>  \n>  cleanup:\n> +\tkh_destroy_str(shown);\n>  \tstrbuf_release(&response_token);\n>  \tstrbuf_release(&requested_token_id);\n>  \tstrbuf_release(&payload);\n> @@ -1405,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n>  done:\n>  \tpthread_cond_destroy(&state.cookies_cond);\n>  \tpthread_mutex_destroy(&state.main_lock);\n> +\thashmap_clear(&state.cookies);\n>  \tfsm_listen__dtor(&state);\n>  \tfsm_health__dtor(&state);\n>  \n\nThese feel like while-at-it memory leak fixes. They should probably be\nmoved into separate commits with a proper explanation.\n\n> diff --git a/compat/fsmonitor/fsm-ipc-linux.c b/compat/fsmonitor/fsm-ipc-linux.c\n> new file mode 100644\n> index 0000000000..d34a6419bc\n> --- /dev/null\n> +++ b/compat/fsmonitor/fsm-ipc-linux.c\n\nThis is (almost) the exact same implementation as we have on macOS. We\nshould probably deduplicate the logic.\n\n> diff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\n> new file mode 100644\n> index 0000000000..04441c5120\n> --- /dev/null\n> +++ b/compat/fsmonitor/fsm-listen-linux.c\n> @@ -0,0 +1,740 @@\n> +#include \"git-compat-util.h\"\n> +#include \"dir.h\"\n> +#include \"fsmonitor-ll.h\"\n> +#include \"fsm-listen.h\"\n> +#include \"fsmonitor--daemon.h\"\n> +#include \"fsmonitor-path-utils.h\"\n> +#include \"gettext.h\"\n> +#include \"simple-ipc.h\"\n> +#include \"string-list.h\"\n> +#include \"trace.h\"\n> +\n> +#include <dirent.h>\n> +#include <fcntl.h>\n> +#include <poll.h>\n> +#include <sys/inotify.h>\n> +#include <sys/stat.h>\n\nFrom these we should really only require <sys/inotify.h>, as all others\nare included via \"compat/posix.h\"\n\n> +/*\n> + * Register an inotify watch, add watch descriptor to path mapping\n> + * and the reverse mapping.\n> + */\n> +static int add_watch(const char *path, struct fsm_listen_data *data)\n> +{\n> +\tconst char *interned = strintern(path);\n> +\tstruct watch_entry *w1, *w2;\n> +\n> +\t/* add the inotify watch, don't allow watches to be modified */\n> +\tint wd = inotify_add_watch(data->fd_inotify, interned,\n> +\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n> +\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n> +\tif (wd < 0) {\n> +\t\tif (errno == ENOENT || errno == ENOTDIR)\n> +\t\t\treturn 0; /* directory was deleted or is not a directory */\n> +\t\tif (errno == EEXIST)\n> +\t\t\treturn 0; /* watch already exists, no action needed */\n\nHm. In case the watch already exists, can't it be the case that we have\nraced e.g. during a rename? For example, the old watch hasn't been\nremoved yet, but we already try to create the new watch. If we then see\nEEXIST we wouldn't update the hash map and thus keep the old path\nintact.\n\nOr is the order of inotify events guaranteed?\n\n> +\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n> +\t}\n> +\n> +\t/* add watch descriptor -> directory mapping */\n> +\tCALLOC_ARRAY(w1, 1);\n> +\tw1->wd = wd;\n> +\tw1->dir = interned;\n> +\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n> +\thashmap_add(&data->watches, &w1->ent);\n> +\n> +\t/* add directory -> watch descriptor mapping */\n> +\tCALLOC_ARRAY(w2, 1);\n> +\tw2->wd = wd;\n> +\tw2->dir = interned;\n> +\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n> +\thashmap_add(&data->revwatches, &w2->ent);\n\nWe have to create two watch entries here, which is a bit puzzling at\nfirst as you'd expect that we can simply sotre the same entry twice. But\nthis is a limitation of our hashmap interface.\n\n> +\treturn 0;\n> +}\n> +\n> +/*\n> + * Remove the inotify watch, the watch descriptor to path mapping\n> + * and the reverse mapping.\n> + */\n> +static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n> +{\n> +\tstruct watch_entry k1, k2, *w1, *w2;\n> +\n> +\t/* remove watch, ignore error if kernel already did it */\n> +\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n> +\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n> +\n> +\tk1.wd = w->wd;\n> +\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n> +\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n> +\tif (!w1)\n> +\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n\nError messages should start with a lower-case letter.\n\n> +/*\n> + * Recursively add watches to every directory under path\n> + */\n> +static int register_inotify(const char *path,\n> +\t\t\t    struct fsmonitor_daemon_state *state,\n> +\t\t\t    struct fsmonitor_batch *batch)\n> +{\n> +\tDIR *dir;\n> +\tconst char *rel;\n> +\tstruct strbuf current = STRBUF_INIT;\n> +\tstruct dirent *de;\n> +\tstruct stat fs;\n> +\tint ret = -1;\n> +\n> +\tdir = opendir(path);\n> +\tif (!dir) {\n> +\t\tif (errno == ENOENT || errno == ENOTDIR)\n> +\t\t\treturn 0; /* directory was deleted */\n\nIs it correct to conflate ENOENT and ENOTDIR here? In the first case the\ndirectory was deleted, sure. But in the second case the directory might\nhave turned into a file due to renames, so don't we have to treat it a\nbit differently?\n\n> +\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n> +\t}\n> +\n> +\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n> +\t\tstrbuf_reset(&current);\n> +\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n> +\t\tif (lstat(current.buf, &fs)) {\n> +\t\t\tif (errno == ENOENT)\n> +\t\t\t\tcontinue; /* file was deleted */\n> +\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n> +\t\t\tgoto failed;\n> +\t\t}\n\nWe don't use fstatat(3p) in our codebase yet, also because it's\nnot easily portable to Windows. But I wonder whether we should use it\nhere to be safer against races.\n\n> +/*\n> + * Process a single inotify event and queue for publication.\n> + */\n> +static int process_event(const char *path,\n> +\t\t\t const struct inotify_event *event,\n> +\t\t\t struct fsmonitor_batch **batch,\n> +\t\t\t struct string_list *cookie_list,\n> +\t\t\t struct fsmonitor_daemon_state *state)\n> +{\n> +\tconst char *rel;\n> +\tconst char *last_sep;\n> +\n> +\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n> +\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n> +\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n> +\t\t/* Use just the filename of the cookie file. */\n> +\t\tlast_sep = find_last_dir_sep(path);\n> +\t\tstring_list_append(cookie_list,\n> +\t\t\t\t   last_sep ? last_sep + 1 : path);\n> +\t\tbreak;\n> +\tcase IS_INSIDE_DOT_GIT:\n> +\tcase IS_INSIDE_GITDIR:\n> +\t\tbreak;\n> +\tcase IS_DOT_GIT:\n> +\tcase IS_GITDIR:\n> +\t\t/*\n> +\t\t * If .git directory is deleted or renamed away,\n> +\t\t * we have to quit.\n> +\t\t */\n> +\t\tif (em_dir_deleted(event->mask)) {\n> +\t\t\ttrace_printf_key(&trace_fsmonitor,\n> +\t\t\t\t\t \"event: gitdir removed\");\n> +\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n> +\t\t\tgoto done;\n> +\t\t}\n> +\n> +\t\tif (em_dir_renamed(event->mask)) {\n> +\t\t\ttrace_printf_key(&trace_fsmonitor,\n> +\t\t\t\t\t \"event: gitdir renamed\");\n> +\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n> +\t\t\tgoto done;\n> +\t\t}\n> +\t\tbreak;\n> +\tcase IS_WORKDIR_PATH:\n> +\t\t/* normal events in the working directory */\n> +\t\tif (trace_pass_fl(&trace_fsmonitor))\n> +\t\t\tlog_mask_set(path, event->mask);\n> +\n> +\t\tif (!*batch)\n> +\t\t\t*batch = fsmonitor_batch__new();\n> +\n> +\t\trel = path + state->path_worktree_watch.len + 1;\n> +\t\tfsmonitor_batch__add_path(*batch, rel);\n> +\n> +\t\tif (em_dir_deleted(event->mask))\n> +\t\t\tbreak;\n\nCurious. Don't we have to unregister the watcher in case a directory was\ndeleted?\n\n> +/*\n> + * Read the inotify event stream and pre-process events before further\n> + * processing and eventual publishing.\n> + */\n> +static void handle_events(struct fsmonitor_daemon_state *state)\n> +{\n> +\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n> +\tchar buf[4096]\n> +\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n> +\n> +\tstruct hashmap *watches = &state->listen_data->watches;\n> +\tstruct fsmonitor_batch *batch = NULL;\n> +\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n> +\tstruct watch_entry k, *w;\n> +\tstruct strbuf path = STRBUF_INIT;\n> +\tconst struct inotify_event *event;\n> +\tint fd = state->listen_data->fd_inotify;\n> +\tssize_t len;\n> +\tchar *ptr, *p;\n> +\n> +\tfor (;;) {\n> +\t\tlen = read(fd, buf, sizeof(buf));\n> +\t\tif (len == -1) {\n> +\t\t\tif (errno == EAGAIN || errno == EINTR)\n> +\t\t\t\tgoto done;\n> +\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n> +\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n> +\t\t\tgoto done;\n> +\t\t}\n> +\n> +\t\t/* nothing to read */\n> +\t\tif (len == 0)\n> +\t\t\tgoto done;\n> +\n> +\t\t/* Loop over all events in the buffer. */\n> +\t\tfor (ptr = buf; ptr < buf + len;\n> +\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n> +\n> +\t\t\tevent = (const struct inotify_event *)ptr;\n> +\n> +\t\t\tif (em_ignore(event->mask))\n> +\t\t\t\tcontinue;\n> +\n> +\t\t\t/* File system was unmounted or event queue overflowed */\n> +\t\t\tif (em_force_shutdown(event->mask)) {\n> +\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n> +\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n> +\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n> +\t\t\t\tgoto done;\n> +\t\t\t}\n> +\n> +\t\t\tk.wd = event->wd;\n> +\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n> +\n> +\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n> +\t\t\tif (!w) {\n> +\t\t\t\t/* Watch was removed, skip event */\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\n> +\t\t\t/* directory watch was removed */\n> +\t\t\tif (em_remove_watch(event->mask)) {\n> +\t\t\t\tremove_watch(w, state->listen_data);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n\nCan it happen that events arrive out-of-order so that we have some\nevents queued up that would touch the same path? In such a case we might\nsilently ignore such queued events.\n\n> +/*\n> + * Non-blocking read of the inotify events stream. The inotify fd is polled\n> + * frequently to help minimize the number of queue overflows.\n> + */\n> +void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n> +{\n> +\tint poll_num;\n> +\tconst int interval = 1000;\n> +\ttime_t checked = time(NULL);\n\nDo we need to use a monotonic clock here to ensure that there cannot be\nany backwards jumps in time, e.g. when switching from summer to winter\ntime?\n\n> +\tstruct pollfd fds[1];\n> +\n> +\tfds[0].fd = state->listen_data->fd_inotify;\n> +\tfds[0].events = POLLIN;\n> +\n> +\t/*\n> +\t * Our fs event listener is now running, so it's safe to start\n> +\t * serving client requests.\n> +\t */\n> +\tipc_server_start_async(state->ipc_server_data);\n> +\n> +\tfor (;;) {\n> +\t\tswitch (state->listen_data->shutdown) {\n\nDo we have to synchronize access to `state->listen_data->shutdown`? As\nfar as I can see it's being set by the fs event listener.\n\n> +\t\tcase SHUTDOWN_CONTINUE:\n> +\t\t\tpoll_num = poll(fds, 1, 1);\n> +\t\t\tif (poll_num == -1) {\n> +\t\t\t\tif (errno == EINTR)\n> +\t\t\t\t\tcontinue;\n> +\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n> +\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\n> +\t\t\tif ((time(NULL) - checked) >= interval) {\n\nIs it intended that the polling timeout is 1000 seconds, or ~16 minutes?\nIf so, it feels like something that might warrant a comment.\n\n> diff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\n> new file mode 100644\n> index 0000000000..fc4acbc20d\n> --- /dev/null\n> +++ b/compat/fsmonitor/fsm-path-utils-linux.c\n> @@ -0,0 +1,223 @@\n> +#include \"git-compat-util.h\"\n> +#include \"fsmonitor-ll.h\"\n> +#include \"fsmonitor-path-utils.h\"\n> +#include \"gettext.h\"\n> +#include \"trace.h\"\n> +\n> +#include <errno.h>\n> +#include <stdio.h>\n> +#include <string.h>\n\nThese includes shouldn't be required, as they are already included via\n\"git-compat-util.h\".\n\n[snip]\n> +/*\n> + * Find the mount point for a given path by reading /proc/mounts.\n> + * Returns the filesystem type for the longest matching mount point.\n> + */\n> +static char *find_mount(const char *path, struct statfs *fs)\n> +{\n> +\tFILE *fp;\n> +\tstruct strbuf line = STRBUF_INIT;\n> +\tstruct strbuf match = STRBUF_INIT;\n> +\tstruct strbuf fstype = STRBUF_INIT;\n> +\tchar *result = NULL;\n> +\tstruct statfs path_fs;\n> +\n> +\tif (statfs(path, &path_fs) < 0)\n> +\t\treturn NULL;\n> +\n> +\tfp = fopen(\"/proc/mounts\", \"r\");\n> +\tif (!fp)\n> +\t\treturn NULL;\n\nIn which cases do we need to have this fallback for statfs? This syscall\nexists in Linux since the 90s, so shouldn't we be able to assume that we\ncan use it? Or are there specific error cases that we need to worry\nabout here?\n\n> diff --git a/compat/fsmonitor/fsm-settings-linux.c b/compat/fsmonitor/fsm-settings-linux.c\n> new file mode 100644\n> index 0000000000..23e7442d0c\n> --- /dev/null\n> +++ b/compat/fsmonitor/fsm-settings-linux.c\n\nThis file is again an almost exact copy of what we have in\n\"fsm-settings-darwin.c\", and as far as I can see there isn't even\nanything specific to either of the systems here. So we should probably\ndeduplicate the logic.\n\n> diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\n> index 409cd0cd12..d2f1f1097e 100755\n> --- a/t/t7527-builtin-fsmonitor.sh\n> +++ b/t/t7527-builtin-fsmonitor.sh\n> @@ -520,6 +520,28 @@ test_expect_success 'directory changes to a file' '\n>  \tgrep \"^event: dir1$\" .git/trace\n>  '\n>  \n> +test_expect_success 'rapid nested directory creation' '\n> +\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n> +\n> +\tstart_daemon --tf \"$PWD/.git/trace\" &&\n> +\n> +\t# Rapidly create nested directories to exercise race conditions\n> +\t# where directory watches may be added concurrently during\n> +\t# event processing and recursive scanning.\n> +\tfor i in $(test_seq 1 20)\n> +\tdo\n> +\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n> +\tdone &&\n> +\n> +\t# Give the daemon time to process all events\n> +\tsleep 1 &&\n> +\n> +\ttest-tool fsmonitor-client query --token 0 &&\n> +\n> +\t# Verify daemon is still running (did not crash)\n> +\tgit fsmonitor--daemon status\n> +'\n\nIt's a bit unclear why specifically this test was added. Does it catch\nan edge case that you have discovered? Might make sense to also add it\nin a preparatory commit so that we can get a bit of context.\n"},{"id":"537133","messageId":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v5.git.git.1771896704209.gitgitgadget@gmail.com","subject":"[PATCH v6 00/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:06Z","receivedAt":"2026-02-25T20:17:19Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (10):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: deduplicate IPC path logic for Unix platforms\n  fsmonitor: deduplicate settings logic for Unix platforms\n  fsmonitor: implement filesystem change listener for Linux\n  fsmonitor: add tests for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |  12 +-\n builtin/fsmonitor--daemon.c                   |  71 +-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   4 +-\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 220 ++++++\n ...-settings-darwin.c => fsm-settings-unix.c} |  24 +-\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  10 +\n contrib/buildsystems/CMakeLists.txt           |  14 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  17 +-\n run-command.c                                 |  11 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  | 101 ++-\n 18 files changed, 1286 insertions(+), 49 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (96%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (82%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v6\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v5:\n\n  -:  ---------- >  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  -:  ---------- >  2:  d0bd3e32ca fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  -:  ---------- >  3:  d2c5ca0939 compat/win32: add pthread_cond_timedwait\n  -:  ---------- >  4:  0051a19303 fsmonitor: use pthread_cond_timedwait for cookie wait\n  -:  ---------- >  5:  ff31e359a7 fsmonitor: deduplicate IPC path logic for Unix platforms\n  -:  ---------- >  6:  39da1e6be3 fsmonitor: deduplicate settings logic for Unix platforms\n  1:  c54814eb31 !  7:  4eadc06004 fsmonitor: implement filesystem change listener for Linux\n     @@ Metadata\n       ## Commit message ##\n          fsmonitor: implement filesystem change listener for Linux\n      \n     -    Implement fsmonitor for Linux using the inotify API, bringing it to\n     -    feature parity with existing Windows and macOS implementations.\n     +    Implement the built-in fsmonitor daemon for Linux using the inotify\n     +    API, bringing it to feature parity with the existing Windows and macOS\n     +    implementations.\n      \n     -    The Linux implementation uses inotify to monitor filesystem events.\n     -    Unlike macOS's FSEvents which can watch a single root directory,\n     -    inotify requires registering watches on every directory of interest.\n     -    The implementation carefully handles directory renames and moves\n     -    using inotify's cookie mechanism to track IN_MOVED_FROM/IN_MOVED_TO\n     -    event pairs.\n     +    The implementation uses inotify rather than fanotify because fanotify\n     +    requires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\n     +    unsuitable for an unprivileged user-space daemon.  While inotify has\n     +    the limitation of requiring a separate watch on every directory (unlike\n     +    macOS's FSEvents, which can monitor an entire directory tree with a\n     +    single watch), it operates without elevated privileges and provides\n     +    the per-file event granularity needed for fsmonitor.\n      \n     -    Key implementation details:\n     -    - Uses inotify_init1(O_NONBLOCK) for non-blocking event monitoring\n     -    - Maintains bidirectional hashmaps between watch descriptors and paths\n     -      for efficient event processing\n     -    - Handles directory creation, deletion, and renames dynamically\n     -    - Detects remote filesystems (NFS, CIFS, SMB, etc.) via statfs()\n     -    - Falls back to $HOME/.git-fsmonitor-* for socket when .git is remote\n     +    The listener uses inotify_init1(O_NONBLOCK) with a poll loop that\n     +    checks for events with a 50-millisecond timeout, keeping the inotify\n     +    queue well-drained to minimize the risk of overflows.  Bidirectional\n     +    hashmaps map between watch descriptors and directory paths for efficient\n     +    event resolution.  Directory renames are tracked using inotify's cookie\n     +    mechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\n     +    periodic check detects stale renames where the matching IN_MOVED_TO\n     +    never arrived, forcing a resync.\n      \n     -    Build configuration:\n     -    - Enabled via FSMONITOR_DAEMON_BACKEND=linux and FSMONITOR_OS_SETTINGS=linux\n     -    - Requires NO_PTHREADS and NO_UNIX_SOCKETS to be unset\n     -    - Adds HAVE_LINUX_MAGIC_H for filesystem type detection\n     +    New directory creation triggers recursive watch registration to ensure\n     +    all subdirectories are monitored.  The IN_MASK_CREATE flag is used\n     +    where available to prevent modifying existing watches, with a fallback\n     +    for older kernels.  When IN_MASK_CREATE is available and\n     +    inotify_add_watch returns EEXIST, it means another thread or recursive\n     +    scan has already registered the watch, so it is safe to ignore.\n      \n     -    Documentation updated to note that fsmonitor.socketDir is now supported\n     -    on both Mac OS and Linux, and adds a section about inotify watch limits.\n     -\n     -    Issues addressed from PR #1352 (git/git) review comments:\n     -    - GPLv3 ME_REMOTE macro: Rewrote remote filesystem detection from\n     -      scratch using statfs() and linux/magic.h constants (no GPLv3 code)\n     -    - Memory leak on inotify_init1 failure: Added FREE_AND_NULL cleanup\n     -    - Unsafe hashmap iteration in dtor: Collect entries first, then modify\n     -    - Missing null checks in stop_async: Added proper guard conditions\n     -    - dirname() modifying argument: Create copy with xstrdup() first\n     -    - Non-portable f_fsid.__val: Use memcmp() for fsid comparison\n     -    - Missing worktree null check: Added BUG() for null worktree\n     -    - Header updates: Use git-compat-util.h, hash_to_hex_algop()\n     -    - Code style: Use xstrdup() not xmemdupz(), proper pointer style\n     -\n     -    Issues addressed from PR #1667 (git/git) review comments:\n     -    - EINTR handling: read() now handles both EAGAIN and EINTR\n     -    - Trailing pipe in log_mask_set: Added strbuf_strip_suffix()\n     -    - Unchecked add_watch return: Now logs failure in rename_dir()\n     -    - String building: Consolidated strbuf operations with strbuf_addf()\n     -    - Translation markers: Added _() to all error_errno() messages\n     -\n     -    Based on work from https://github.com/git/git/pull/1352 by Eric DeCosta,\n     -    and https://github.com/git/git/pull/1667 by Marziyeh Esipreh, updated to\n     -    work with the current codebase and address all review feedback.\n     +    Remote filesystem detection uses statfs() to identify network-mounted\n     +    filesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\n     +    Mount point information is read from /proc/mounts and matched against\n     +    the statfs f_fsid to get accurate, human-readable filesystem type names\n     +    for logging.  When the .git directory is on a remote filesystem, the\n     +    IPC socket falls back to $HOME or a user-configured directory via the\n     +    fsmonitor.socketDir setting.\n      \n     +    Based-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\n     +    Based-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## Documentation/config/fsmonitor--daemon.adoc ##\n     @@ Documentation/git-fsmonitor--daemon.adoc: By default, the socket is created in t\n       -------------\n       \n      \n     - ## builtin/fsmonitor--daemon.c ##\n     -@@ builtin/fsmonitor--daemon.c: static int do_handle_client(struct fsmonitor_daemon_state *state,\n     - \tconst struct fsmonitor_batch *batch;\n     - \tstruct fsmonitor_batch *remainder = NULL;\n     - \tintmax_t count = 0, duplicates = 0;\n     --\tkh_str_t *shown;\n     -+\tkh_str_t *shown = NULL;\n     - \tint hash_ret;\n     - \tint do_trivial = 0;\n     - \tint do_flush = 0;\n     -@@ builtin/fsmonitor--daemon.c: static int do_handle_client(struct fsmonitor_daemon_state *state,\n     - \t\ttotal_response_len += payload.len;\n     - \t}\n     - \n     --\tkh_release_str(shown);\n     --\n     - \tpthread_mutex_lock(&state->main_lock);\n     - \n     - \tif (token_data->client_ref_count > 0)\n     -@@ builtin/fsmonitor--daemon.c: static int do_handle_client(struct fsmonitor_daemon_state *state,\n     - \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n     - \n     - cleanup:\n     -+\tkh_destroy_str(shown);\n     - \tstrbuf_release(&response_token);\n     - \tstrbuf_release(&requested_token_id);\n     - \tstrbuf_release(&payload);\n     -@@ builtin/fsmonitor--daemon.c: static int fsmonitor_run_daemon(void)\n     - done:\n     - \tpthread_cond_destroy(&state.cookies_cond);\n     - \tpthread_mutex_destroy(&state.main_lock);\n     -+\thashmap_clear(&state.cookies);\n     - \tfsm_listen__dtor(&state);\n     - \tfsm_health__dtor(&state);\n     - \n     -\n       ## compat/fsmonitor/fsm-health-linux.c (new) ##\n      @@\n      +#include \"git-compat-util.h\"\n     @@ compat/fsmonitor/fsm-health-linux.c (new)\n      +\n      +void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n      +{\n     -+}\n     -\n     - ## compat/fsmonitor/fsm-ipc-linux.c (new) ##\n     -@@\n     -+#define USE_THE_REPOSITORY_VARIABLE\n     -+\n     -+#include \"git-compat-util.h\"\n     -+#include \"config.h\"\n     -+#include \"gettext.h\"\n     -+#include \"hex.h\"\n     -+#include \"path.h\"\n     -+#include \"repository.h\"\n     -+#include \"strbuf.h\"\n     -+#include \"fsmonitor-ll.h\"\n     -+#include \"fsmonitor-ipc.h\"\n     -+#include \"fsmonitor-path-utils.h\"\n     -+\n     -+static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, \"fsmonitor--daemon.ipc\")\n     -+\n     -+const char *fsmonitor_ipc__get_path(struct repository *r)\n     -+{\n     -+\tstatic const char *ipc_path = NULL;\n     -+\tgit_SHA_CTX sha1ctx;\n     -+\tchar *sock_dir = NULL;\n     -+\tstruct strbuf ipc_file = STRBUF_INIT;\n     -+\tunsigned char hash[GIT_SHA1_RAWSZ];\n     -+\n     -+\tif (!r)\n     -+\t\tBUG(\"No repository passed into fsmonitor_ipc__get_path\");\n     -+\n     -+\tif (ipc_path)\n     -+\t\treturn ipc_path;\n     -+\n     -+\t/* By default the socket file is created in the .git directory */\n     -+\tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n     -+\t\tipc_path = fsmonitor_ipc__get_default_path();\n     -+\t\treturn ipc_path;\n     -+\t}\n     -+\n     -+\tif (!r->worktree)\n     -+\t\tBUG(\"repository has no worktree\");\n     -+\n     -+\tgit_SHA1_Init(&sha1ctx);\n     -+\tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n     -+\tgit_SHA1_Final(hash, &sha1ctx);\n     -+\n     -+\trepo_config_get_string(r, \"fsmonitor.socketdir\", &sock_dir);\n     -+\n     -+\t/* Create the socket file in either socketDir or $HOME */\n     -+\tif (sock_dir && *sock_dir) {\n     -+\t\tstrbuf_addf(&ipc_file, \"%s/.git-fsmonitor-%s\",\n     -+\t\t\t    sock_dir, hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n     -+\t} else {\n     -+\t\tstrbuf_addf(&ipc_file, \"~/.git-fsmonitor-%s\",\n     -+\t\t\t    hash_to_hex_algop(hash, &hash_algos[GIT_HASH_SHA1]));\n     -+\t}\n     -+\tfree(sock_dir);\n     -+\n     -+\tipc_path = interpolate_path(ipc_file.buf, 1);\n     -+\tif (!ipc_path)\n     -+\t\tdie(_(\"Invalid path: %s\"), ipc_file.buf);\n     -+\n     -+\tstrbuf_release(&ipc_file);\n     -+\treturn ipc_path;\n      +}\n      \n       ## compat/fsmonitor/fsm-listen-linux.c (new) ##\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +#include \"string-list.h\"\n      +#include \"trace.h\"\n      +\n     -+#include <dirent.h>\n     -+#include <fcntl.h>\n     -+#include <poll.h>\n      +#include <sys/inotify.h>\n     -+#include <sys/stat.h>\n      +\n      +/*\n      + * Safe value to bitwise OR with rest of mask for\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\t\t\treturn 0; /* directory was deleted or is not a directory */\n      +\t\tif (errno == EEXIST)\n      +\t\t\treturn 0; /* watch already exists, no action needed */\n     ++\t\tif (errno == ENOSPC)\n     ++\t\t\treturn error(_(\"inotify watch limit reached; \"\n     ++\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n      +\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n      +\t}\n      +\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n      +\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n      +\tif (!w1)\n     -+\t\tBUG(\"Double remove of watch for '%s'\", w->dir);\n     ++\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n      +\n      +\tif (w1->cookie)\n     -+\t\tBUG(\"Removing watch for '%s' which has a pending rename\", w1->dir);\n     ++\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n      +\n      +\tk2.dir = w->dir;\n      +\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n      +\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n      +\tif (!w2)\n     -+\t\tBUG(\"Double remove of reverse watch for '%s'\", w->dir);\n     ++\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n      +\n      +\t/* w1->dir and w2->dir are interned strings, we don't own them */\n      +\tfree(w1);\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\t\t} else {\n      +\t\t\t/* Directory was moved out of watch tree */\n      +\t\t\ttrace_printf_key(&trace_fsmonitor,\n     -+\t\t\t\t\t \"No matching watch for rename to '%s'\", path);\n     ++\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n      +\t\t}\n      +\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n      +\t\tfree(re);\n      +\t} else {\n      +\t\t/* Directory was moved from outside the watch tree */\n      +\t\ttrace_printf_key(&trace_fsmonitor,\n     -+\t\t\t\t \"No matching cookie for rename to '%s'\", path);\n     ++\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n      +\t}\n      +}\n      +\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\tCALLOC_ARRAY(data, 1);\n      +\tstate->listen_data = data;\n      +\tstate->listen_error_code = -1;\n     ++\tdata->fd_inotify = -1;\n      +\tdata->shutdown = SHUTDOWN_ERROR;\n      +\n      +\tfd = inotify_init1(O_NONBLOCK);\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\n      +\tFREE_AND_NULL(state->listen_data);\n      +\n     -+\tif (fd && (close(fd) < 0))\n     ++\tif (fd >= 0 && (close(fd) < 0))\n      +\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n      +}\n      +\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\t\t\tif (errno == EAGAIN || errno == EINTR)\n      +\t\t\t\tgoto done;\n      +\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n     -+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n     ++\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n      +\t\t\tgoto done;\n      +\t\t}\n      +\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n      +{\n      +\tint poll_num;\n     -+\tconst int interval = 1000;\n     ++\t/*\n     ++\t * Interval in seconds between checks for stale directory renames.\n     ++\t * A directory rename that is not completed within this window\n     ++\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n     ++\t * missed events, forcing a shutdown.\n     ++\t */\n     ++\tconst int interval = 1;\n      +\ttime_t checked = time(NULL);\n      +\tstruct pollfd fds[1];\n      +\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\tfor (;;) {\n      +\t\tswitch (state->listen_data->shutdown) {\n      +\t\tcase SHUTDOWN_CONTINUE:\n     -+\t\t\tpoll_num = poll(fds, 1, 1);\n     ++\t\t\tpoll_num = poll(fds, 1, 50);\n      +\t\t\tif (poll_num == -1) {\n      +\t\t\t\tif (errno == EINTR)\n      +\t\t\t\t\tcontinue;\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n      +\t\t\t\t\t\t\t    checked - interval)) {\n      +\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n     -+\t\t\t\t\t\t\t \"Missed IN_MOVED_TO events, forcing shutdown\");\n     ++\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n      +\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n      +\t\t\t\t\tcontinue;\n      +\t\t\t\t}\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +#include \"gettext.h\"\n      +#include \"trace.h\"\n      +\n     -+#include <errno.h>\n     -+#include <stdio.h>\n     -+#include <string.h>\n      +#include <sys/statfs.h>\n      +\n      +#ifdef HAVE_LINUX_MAGIC_H\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +\t\t\t       const struct alias_info *info UNUSED)\n      +{\n      +\treturn NULL;\n     -+}\n     -\n     - ## compat/fsmonitor/fsm-settings-linux.c (new) ##\n     -@@\n     -+#include \"git-compat-util.h\"\n     -+#include \"config.h\"\n     -+#include \"fsmonitor-ll.h\"\n     -+#include \"fsmonitor-ipc.h\"\n     -+#include \"fsmonitor-settings.h\"\n     -+#include \"fsmonitor-path-utils.h\"\n     -+\n     -+#include <libgen.h>\n     -+\n     -+/*\n     -+ * For the builtin FSMonitor, we create the Unix domain socket for the\n     -+ * IPC in the .git directory.  If the working directory is remote,\n     -+ * then the socket will be created on the remote file system.  This\n     -+ * can fail if the remote file system does not support UDS file types\n     -+ * (e.g. smbfs to a Windows server) or if the remote kernel does not\n     -+ * allow a non-local process to bind() the socket.  (These problems\n     -+ * could be fixed by moving the UDS out of the .git directory and to a\n     -+ * well-known local directory on the client machine, but care should\n     -+ * be taken to ensure that $HOME is actually local and not a managed\n     -+ * file share.)\n     -+ *\n     -+ * FAT32 and NTFS working directories are problematic too.\n     -+ *\n     -+ * The builtin FSMonitor uses a Unix domain socket in the .git\n     -+ * directory for IPC.  These Windows drive formats do not support\n     -+ * Unix domain sockets, so mark them as incompatible for the daemon.\n     -+ */\n     -+static enum fsmonitor_reason check_uds_volume(struct repository *r)\n     -+{\n     -+\tstruct fs_info fs;\n     -+\tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n     -+\tchar *path;\n     -+\tchar *dir;\n     -+\n     -+\t/*\n     -+\t * Create a copy for dirname() since it may modify its argument.\n     -+\t */\n     -+\tpath = xstrdup(ipc_path);\n     -+\tdir = dirname(path);\n     -+\n     -+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n     -+\t\tfree(path);\n     -+\t\treturn FSMONITOR_REASON_ERROR;\n     -+\t}\n     -+\n     -+\tfree(path);\n     -+\n     -+\tif (fs.is_remote ||\n     -+\t    !strcmp(fs.typename, \"msdos\") ||\n     -+\t    !strcmp(fs.typename, \"ntfs\") ||\n     -+\t    !strcmp(fs.typename, \"vfat\")) {\n     -+\t\tfree(fs.typename);\n     -+\t\treturn FSMONITOR_REASON_NOSOCKETS;\n     -+\t}\n     -+\n     -+\tfree(fs.typename);\n     -+\treturn FSMONITOR_REASON_OK;\n     -+}\n     -+\n     -+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)\n     -+{\n     -+\tenum fsmonitor_reason reason;\n     -+\n     -+\tif (ipc) {\n     -+\t\treason = check_uds_volume(r);\n     -+\t\tif (reason != FSMONITOR_REASON_OK)\n     -+\t\t\treturn reason;\n     -+\t}\n     -+\n     -+\treturn FSMONITOR_REASON_OK;\n      +}\n      \n       ## config.mak.uname ##\n     @@ config.mak.uname: ifeq ($(uname_S),Linux)\n      \n       ## contrib/buildsystems/CMakeLists.txt ##\n      @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n     + \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n     + \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n       \n     - \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n     ++\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n      +\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n      +\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n      +\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n      +\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n      +\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n      +\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n      +\n     -+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)\n     + \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     + \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n       \tendif()\n     - endif()\n     - \n      \n       ## meson.build ##\n      @@ meson.build: endif\n     @@ meson.build: endif\n       elif host_machine.system() == 'darwin'\n         fsmonitor_backend = 'darwin'\n         libgit_dependencies += dependency('CoreServices')\n     -\n     - ## t/t7527-builtin-fsmonitor.sh ##\n     -@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'directory changes to a file' '\n     - \tgrep \"^event: dir1$\" .git/trace\n     - '\n     - \n     -+test_expect_success 'rapid nested directory creation' '\n     -+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n     -+\n     -+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n     -+\n     -+\t# Rapidly create nested directories to exercise race conditions\n     -+\t# where directory watches may be added concurrently during\n     -+\t# event processing and recursive scanning.\n     -+\tfor i in $(test_seq 1 20)\n     -+\tdo\n     -+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n     -+\tdone &&\n     -+\n     -+\t# Give the daemon time to process all events\n     -+\tsleep 1 &&\n     -+\n     -+\ttest-tool fsmonitor-client query --token 0 &&\n     -+\n     -+\t# Verify daemon is still running (did not crash)\n     -+\tgit fsmonitor--daemon status\n     -+'\n     -+\n     - # The next few test cases exercise the token-resync code.  When filesystem\n     - # drops events (because of filesystem velocity or because the daemon isn't\n     - # polling fast enough), we need to discard the cached data (relative to the\n  -:  ---------- >  8:  8fec92d5b4 fsmonitor: add tests for Linux\n  -:  ---------- >  9:  817489b3ea run-command: add close_fd_above_stderr option\n  -:  ---------- > 10:  bb438afbbe fsmonitor: close inherited file descriptors and detach in daemon\n\n-- \ngitgitgadget\n"},{"id":"537132","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 01/10] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:07Z","receivedAt":"2026-02-25T20:17:20Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"537134","messageId":"d0bd3e32ca32b4150054ea91aa774a5b2db427e5.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 02/10] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:08Z","receivedAt":"2026-02-25T20:17:21Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  Add a hashmap_clear() call to prevent this\nmemory leak.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..4d52622e24 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\thashmap_clear(&state.cookies);\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"537135","messageId":"d2c5ca09396e020adb717055d82f50de7c1b7431.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 03/10] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:09Z","receivedAt":"2026-02-25T20:17:23Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nThis enables callers to use bounded waits on condition variables\ninstead of blocking indefinitely with pthread_cond_wait().\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..538ef92d9d 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\ttimeout_ms = 0;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"537136","messageId":"0051a1930349878fd25bb5d2240073beef36da7d.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:10Z","receivedAt":"2026-02-25T20:17:24Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 4d52622e24..f6c406ff12 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to see the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"537137","messageId":"ff31e359a7070c8d410518da0230ad0ecae7d771.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 05/10] fsmonitor: deduplicate IPC path logic for Unix platforms","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:11Z","receivedAt":"2026-02-25T20:17:25Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe IPC path logic for determining the Unix domain socket location is\nnearly identical between macOS and Linux.  Both need to check whether\nthe .git directory is on a remote filesystem and, if so, fall back to\na socket path under $HOME or a user-configured directory.\n\nMerge the two implementations into a single fsm-ipc-unix.c that is\nshared by both platforms.  The unified version includes the worktree\nNULL check (BUG guard) from the Linux implementation, which was missing\nin the macOS version.\n\nUpdate Makefile, meson.build, and CMakeLists.txt to use the new shared\nfile for non-Windows platforms.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                              | 6 +++++-\n compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} | 4 +++-\n contrib/buildsystems/CMakeLists.txt                   | 2 +-\n meson.build                                           | 7 ++++++-\n 4 files changed, 15 insertions(+), 4 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (96%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..2157bbf173 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2365,7 +2365,11 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n+ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n+else\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n+endif\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 96%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\nindex fe149a1b37..d34a6419bc 100644\n--- a/compat/fsmonitor/fsm-ipc-darwin.c\n+++ b/compat/fsmonitor/fsm-ipc-unix.c\n@@ -27,13 +27,15 @@ const char *fsmonitor_ipc__get_path(struct repository *r)\n \tif (ipc_path)\n \t\treturn ipc_path;\n \n-\n \t/* By default the socket file is created in the .git directory */\n \tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n \t\tipc_path = fsmonitor_ipc__get_default_path();\n \t\treturn ipc_path;\n \t}\n \n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n \tgit_SHA1_Init(&sha1ctx);\n \tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n \tgit_SHA1_Final(hash, &sha1ctx);\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..32ef6ebe1b 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -303,7 +303,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..8de795f9d4 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1332,11 +1332,16 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n+\n+  if fsmonitor_backend == 'win32'\n+    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n+  else\n+    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n+  endif\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537138","messageId":"39da1e6be359a23ddad8f099d427823a774324f7.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 06/10] fsmonitor: deduplicate settings logic for Unix platforms","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:12Z","receivedAt":"2026-02-25T20:17:26Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic for checking Unix domain socket\ncompatibility is nearly identical between macOS and Linux.  Both check\nwhether the IPC socket directory resides on a remote filesystem or a\nfilesystem that does not support Unix domain sockets (NTFS, FAT32, etc).\n\nMerge the two implementations into a single fsm-settings-unix.c shared\nby both platforms.  The unified version uses the safer xstrdup() +\ndirname() approach from the macOS implementation (avoiding strbuf\nmutation with dirname()) and includes the \"vfat\" filesystem check.\n\nUpdate Makefile, meson.build, and CMakeLists.txt to use the new shared\nfile for non-Windows platforms.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++++-\n ...-settings-darwin.c => fsm-settings-unix.c} | 24 ++++++++++++-------\n contrib/buildsystems/CMakeLists.txt           |  2 +-\n meson.build                                   | 11 ++++++---\n 4 files changed, 29 insertions(+), 14 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (82%)\n\ndiff --git a/Makefile b/Makefile\nindex 2157bbf173..febdaeb42c 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2374,7 +2374,11 @@ endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n+ifeq ($(FSMONITOR_OS_SETTINGS),win32)\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-win32.o\n+else\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-unix.o\n+endif\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n endif\n \ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 82%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\nindex a382590635..27d89207af 100644\n--- a/compat/fsmonitor/fsm-settings-darwin.c\n+++ b/compat/fsmonitor/fsm-settings-unix.c\n@@ -5,7 +5,7 @@\n #include \"fsmonitor-settings.h\"\n #include \"fsmonitor-path-utils.h\"\n \n- /*\n+/*\n  * For the builtin FSMonitor, we create the Unix domain socket for the\n  * IPC in the .git directory.  If the working directory is remote,\n  * then the socket will be created on the remote file system.  This\n@@ -22,25 +22,31 @@\n  * The builtin FSMonitor uses a Unix domain socket in the .git\n  * directory for IPC.  These Windows drive formats do not support\n  * Unix domain sockets, so mark them as incompatible for the daemon.\n- *\n  */\n static enum fsmonitor_reason check_uds_volume(struct repository *r)\n {\n \tstruct fs_info fs;\n \tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n-\tstruct strbuf path = STRBUF_INIT;\n-\tstrbuf_add(&path, ipc_path, strlen(ipc_path));\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n \n-\tif (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {\n-\t\tstrbuf_release(&path);\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n \t\treturn FSMONITOR_REASON_ERROR;\n \t}\n \n-\tstrbuf_release(&path);\n+\tfree(path);\n \n \tif (fs.is_remote ||\n-\t\t!strcmp(fs.typename, \"msdos\") ||\n-\t\t!strcmp(fs.typename, \"ntfs\")) {\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n \t\tfree(fs.typename);\n \t\treturn FSMONITOR_REASON_NOSOCKETS;\n \t}\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 32ef6ebe1b..0eba0c2c98 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -307,7 +307,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 8de795f9d4..e02f9708da 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1334,13 +1334,18 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n \n   if fsmonitor_backend == 'win32'\n-    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n+    libgit_sources += [\n+      'compat/fsmonitor/fsm-ipc-win32.c',\n+      'compat/fsmonitor/fsm-settings-win32.c',\n+    ]\n   else\n-    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n+    libgit_sources += [\n+      'compat/fsmonitor/fsm-ipc-unix.c',\n+      'compat/fsmonitor/fsm-settings-unix.c',\n+    ]\n   endif\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537140","messageId":"4eadc06004c775647e83529dbe057e99cf7f9906.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 07/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:13Z","receivedAt":"2026-02-25T20:17:28Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 220 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |  10 +\n meson.build                                 |   3 +\n 8 files changed, 1048 insertions(+), 6 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..36a701c06a\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..b4c19e0655\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,220 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..e5d79493e5 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = linux\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 0eba0c2c98..8b4387c5a1 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,6 +306,16 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n \n+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n+\n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n \tendif()\ndiff --git a/meson.build b/meson.build\nindex e02f9708da..5661df7b3d 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1322,6 +1322,9 @@ endif\n fsmonitor_backend = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   libgit_dependencies += dependency('CoreServices')\n-- \ngitgitgadget\n\n"},{"id":"537139","messageId":"8fec92d5b4c86da5f85797516c10812150fed557.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 08/10] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:14Z","receivedAt":"2026-02-25T20:17:29Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/meson.build                |  8 +++-\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 2 files changed, 89 insertions(+), 8 deletions(-)\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 19e8306298..85ef2ae2fa 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1210,12 +1210,18 @@ test_environment = script_environment\n test_environment.set('GIT_BUILD_DIR', git_build_dir)\n \n foreach integration_test : integration_tests\n+  per_test_kwargs = test_kwargs\n+  # The fsmonitor tests start daemon processes that in some environments\n+  # can hang.  Set a generous timeout to prevent CI from blocking.\n+  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n+    per_test_kwargs += {'timeout': 1800}\n+  endif\n   test(fs.stem(integration_test), shell,\n     args: [ integration_test ],\n     workdir: meson.current_source_dir(),\n     env: test_environment,\n     depends: test_dependencies + bin_wrappers,\n-    kwargs: test_kwargs,\n+    kwargs: per_test_kwargs,\n   )\n endforeach\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"537141","messageId":"817489b3ea6a0831e4b815df1c4a0c0e100a2683.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 09/10] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:15Z","receivedAt":"2026-02-25T20:17:30Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a new option to struct child_process that closes file descriptors\n3 and above in the child after forking but before exec.  This prevents\nlong-running child processes from inheriting pipe endpoints or other\ndescriptors from the parent environment.\n\nThe upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\ncapped at 4096 to avoid excessive iteration when the limit is set\nvery high.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 11 +++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 20 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..cbadcf5ff8 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -832,6 +832,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..a1aa1b1069 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the long-running child from\n+\t * inheriting pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"537142","messageId":"bb438afbbe3e8bbc2ed048930ef11fb02eb4163c.1772050636.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v6 10/10] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T20:17:16Z","receivedAt":"2026-02-25T20:17:31Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at daemon startup so\nthat file descriptors 3 and above are closed before any daemon work\nbegins.  This ensures the daemon does not inadvertently hold open\ndescriptors from its launching environment.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  Without this, shells that\nenable job control (e.g. bash with \"set -m\") treat the daemon as part\nof the spawning command's job.  Their \"wait\" builtin then blocks until\nthe daemon exits, which it never does.  This specifically affects\nsystems where /bin/sh is bash (e.g. Fedora), since dash only waits for\nthe specific PID rather than the full process group.\n\nAdd a 30-second timeout to \"fsmonitor--daemon stop\" so it does\nnot block indefinitely if the daemon fails to shut down.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c  | 28 +++++++++++++++++++++++++---\n fsmonitor-ipc.c              |  3 +++\n t/meson.build                |  8 +-------\n t/t7527-builtin-fsmonitor.sh | 12 +++++++++++-\n 4 files changed, 40 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex f6c406ff12..4ed848e79e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n@@ -1431,7 +1441,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1451,10 +1461,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1517,6 +1538,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 85ef2ae2fa..19e8306298 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1210,18 +1210,12 @@ test_environment = script_environment\n test_environment.set('GIT_BUILD_DIR', git_build_dir)\n \n foreach integration_test : integration_tests\n-  per_test_kwargs = test_kwargs\n-  # The fsmonitor tests start daemon processes that in some environments\n-  # can hang.  Set a generous timeout to prevent CI from blocking.\n-  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n-    per_test_kwargs += {'timeout': 1800}\n-  endif\n   test(fs.stem(integration_test), shell,\n     args: [ integration_test ],\n     workdir: meson.current_source_dir(),\n     env: test_environment,\n     depends: test_dependencies + bin_wrappers,\n-    kwargs: per_test_kwargs,\n+    kwargs: test_kwargs,\n   )\n endforeach\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 774da5ac60..d7e64bcb7a 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -766,7 +766,7 @@ do\n \t\telse\n \t\t\ttest_expect_success \"Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor\" '\n \t\t\t\tgit config core.fsmonitor true &&\n-\t\t\t\tgit fsmonitor--daemon start &&\n+\t\t\t\tgit fsmonitor--daemon start --start-timeout=10 &&\n \t\t\t\tgit update-index --fsmonitor\n \t\t\t'\n \t\tfi\n@@ -997,7 +997,17 @@ start_git_in_background () {\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n \tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n+\n+\t# Disable job control before wait.  With \"set -m\", bash treats\n+\t# \"wait $pid\" as waiting for the entire job (process group),\n+\t# which blocks indefinitely if the fsmonitor daemon was spawned\n+\t# into the same process group and is still running.  Turning off\n+\t# job control makes \"wait\" only wait for the specific PID.\n+\tset +m &&\n \twait $git_pid\n+\twait_status=$?\n+\tset -m\n+\treturn $wait_status\n }\n \n stop_git () {\n-- \ngitgitgadget\n"},{"id":"537144","messageId":"xmqq4in4a673.fsf@gitster.g","threadId":"64696","inReplyTo":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772050636.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 01/10] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:01:20Z","receivedAt":"2026-02-25T21:01:22Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> The `shown` kh_str_t was freed with kh_release_str() at a point in\n> the code only reachable in the non-trivial response path.  When the\n> client receives a trivial response, the code jumps to the `cleanup`\n> label, skipping the kh_release_str() call entirely and leaking the\n> hash table.\n>\n> Fix this by initializing `shown` to NULL and moving the cleanup to the\n> `cleanup` label using kh_destroy_str(), which is safe to call on NULL.\n> This ensures the hash table is freed regardless of which code path is\n> taken.\n\nMakes perfect sense, the changes to the code matches the\ndescription, and the difference between kh_release_* and\nkh_destroy_* in khash.h is exactly as described in the log message.\n\nPerfect.  I wish all the patches posted here are as easy to review\nas this one ;-)\n\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  builtin/fsmonitor--daemon.c | 5 ++---\n>  1 file changed, 2 insertions(+), 3 deletions(-)\n>\n> diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\n> index 242c594646..bc4571938c 100644\n> --- a/builtin/fsmonitor--daemon.c\n> +++ b/builtin/fsmonitor--daemon.c\n> @@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \tconst struct fsmonitor_batch *batch;\n>  \tstruct fsmonitor_batch *remainder = NULL;\n>  \tintmax_t count = 0, duplicates = 0;\n> -\tkh_str_t *shown;\n> +\tkh_str_t *shown = NULL;\n>  \tint hash_ret;\n>  \tint do_trivial = 0;\n>  \tint do_flush = 0;\n> @@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \t\ttotal_response_len += payload.len;\n>  \t}\n>  \n> -\tkh_release_str(shown);\n> -\n>  \tpthread_mutex_lock(&state->main_lock);\n>  \n>  \tif (token_data->client_ref_count > 0)\n> @@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n>  \n>  cleanup:\n> +\tkh_destroy_str(shown);\n>  \tstrbuf_release(&response_token);\n>  \tstrbuf_release(&requested_token_id);\n>  \tstrbuf_release(&payload);\n"},{"id":"537145","messageId":"xmqqzf4w8r20.fsf@gitster.g","threadId":"64696","inReplyTo":"0051a1930349878fd25bb5d2240073beef36da7d.1772050636.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:13:43Z","receivedAt":"2026-02-25T21:13:45Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> The cookie wait in with_lock__wait_for_cookie() uses an infinite\n> pthread_cond_wait() loop.  The existing comment notes the desire\n> to switch to pthread_cond_timedwait(), but the routine was not\n> available in git thread-utils.\n>\n> On certain container or overlay filesystems, inotify watches may\n> succeed but events are never delivered.  In this case the daemon\n> would hang indefinitely waiting for the cookie event, which in\n> turn causes the client to hang.\n>\n> Replace the infinite wait with a one-second timeout using\n> pthread_cond_timedwait().  If the timeout fires, report an\n> error and let the client proceed with a trivial (full-scan)\n> response rather than blocking forever.\n\nI cannot convince myself if one-second interval is not too frequent\nto force everybody, including those with working inotify, to poll.\nI wonder if this is something that may want to be configurable (or\nbetter yet, auto-detectable, but that may be wishing for moon).\n\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n>  1 file changed, 24 insertions(+), 13 deletions(-)\n>\n> diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\n> index 4d52622e24..f6c406ff12 100644\n> --- a/builtin/fsmonitor--daemon.c\n> +++ b/builtin/fsmonitor--daemon.c\n> @@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n>  \tunlink(cookie_pathname.buf);\n>  \n>  \t/*\n> -\t * Technically, this is an infinite wait (well, unless another\n> -\t * thread sends us an abort).  I'd like to change this to\n> -\t * use `pthread_cond_timedwait()` and return an error/timeout\n> -\t * and let the caller do the trivial response thing, but we\n> -\t * don't have that routine in our thread-utils.\n> -\t *\n> -\t * After extensive beta testing I'm not really worried about\n> -\t * this.  Also note that the above open() and unlink() calls\n> -\t * will cause at least two FS events on that path, so the odds\n> -\t * of getting stuck are pretty slim.\n> +\t * Wait for the listener thread to see the cookie file.\n> +\t * Time out after a short interval so that the client\n> +\t * does not hang forever if the filesystem does not deliver\n> +\t * events (e.g., on certain container/overlay filesystems\n> +\t * where inotify watches succeed but events never arrive).\n>  \t */\n> -\twhile (cookie->result == FCIR_INIT)\n> -\t\tpthread_cond_wait(&state->cookies_cond,\n> -\t\t\t\t  &state->main_lock);\n> +\t{\n> +\t\tstruct timeval now;\n> +\t\tstruct timespec ts;\n> +\t\tint err = 0;\n> +\n> +\t\tgettimeofday(&now, NULL);\n> +\t\tts.tv_sec = now.tv_sec + 1;\n> +\t\tts.tv_nsec = now.tv_usec * 1000;\n> +\n> +\t\twhile (cookie->result == FCIR_INIT && !err)\n> +\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n> +\t\t\t\t\t\t     &state->main_lock,\n> +\t\t\t\t\t\t     &ts);\n> +\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n> +\t\t\ttrace_printf_key(&trace_fsmonitor,\n> +\t\t\t\t\t \"cookie_wait timed out\");\n> +\t\t\tcookie->result = FCIR_ERROR;\n> +\t\t}\n> +\t}\n>  \n>  done:\n>  \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n"},{"id":"537146","messageId":"xmqqv7fk8qvq.fsf@gitster.g","threadId":"64696","inReplyTo":"0051a1930349878fd25bb5d2240073beef36da7d.1772050636.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:17:29Z","receivedAt":"2026-02-25T21:17:31Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +\t * Wait for the listener thread to see the cookie file.\n\nIs this a complete sentence?  Or perhaps something like \"see\" ->\n\"check\"?\n"},{"id":"537147","messageId":"xmqqqzq88q9u.fsf@gitster.g","threadId":"64696","inReplyTo":"ff31e359a7070c8d410518da0230ad0ecae7d771.1772050636.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 05/10] fsmonitor: deduplicate IPC path logic for Unix platforms","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:30:37Z","receivedAt":"2026-02-25T21:30:39Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> The IPC path logic for determining the Unix domain socket location is\n> nearly identical between macOS and Linux.  Both need to check whether\n> the .git directory is on a remote filesystem and, if so, fall back to\n> a socket path under $HOME or a user-configured directory.\n>\n> Merge the two implementations into a single fsm-ipc-unix.c that is\n> shared by both platforms.  The unified version includes the worktree\n> NULL check (BUG guard) from the Linux implementation, which was missing\n> in the macOS version.\n>\n> Update Makefile, meson.build, and CMakeLists.txt to use the new shared\n> file for non-Windows platforms.\n\nThis sounds as if the patch started with two IPC path logic for\nmacOS and Linux, and the patch removes one of them and updates the\nother one so that both platforms can use the surviving one.\n\nBut the patch seems to indicate somewhat different story.  The code\nbefore this patch started with a single macOS (darwin) one, but\nbecause it is mostly applicable to other UNIX variants as well, the\npatch renames the existing macOS one for unix and makes a small\nadjustment (namely, asserts that r->worktree is not NULL).\n\nPerhaps you started with two separate implementations (possibly with\na new one called UNIX that was added by largely copying and pasting\nfrom the macOS one) and unified them, but that history does not\nexist in this 10-patch series, so the above story would need to be\nrewritten to say what actually is happening in this series, e.g., we\nrealized macOS one is applicable generally for UNIX variants so we\nare renaming it from darwin to unix, or something.\n\n> @@ -2365,7 +2365,11 @@ ifdef FSMONITOR_DAEMON_BACKEND\n>  \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n>  \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n>  \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n> -\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n> +ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n> +\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n> +else\n> +\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n> +endif\n\nMakes me wonder if doing\n\n    #define FSMONITOR_DAEMON_BACKEND unix\n\nfor macOS and then keeping\n\n\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n\nas-is would be cleaner.  \n\nLater, we can add the same \"#define FSMONITOR_DAEMON_BACKEND unix\"\nfor Linux, perhaps?\n\nThis is doubly true when we look at the build recipe changes to the\nmeson one below ...\n\n> diff --git a/meson.build b/meson.build\n> index dd52efd1c8..8de795f9d4 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -1332,11 +1332,16 @@ if fsmonitor_backend != ''\n>  \n>    libgit_sources += [\n>      'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n> -    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n>      'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n>      'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n>      'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n>    ]\n> +\n> +  if fsmonitor_backend == 'win32'\n> +    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n> +  else\n> +    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n> +  endif\n>  endif\n\n... which would not be needed if fsmonitor_backend is set to 'unix'\ninstead of 'darwin'.\n"},{"id":"537148","messageId":"xmqqms0w8q8p.fsf@gitster.g","threadId":"64696","inReplyTo":"39da1e6be359a23ddad8f099d427823a774324f7.1772050636.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 06/10] fsmonitor: deduplicate settings logic for Unix platforms","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:31:18Z","receivedAt":"2026-02-25T21:31:20Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> The fsmonitor settings logic for checking Unix domain socket\n> compatibility is nearly identical between macOS and Linux.  Both check\n> whether the IPC socket directory resides on a remote filesystem or a\n> filesystem that does not support Unix domain sockets (NTFS, FAT32, etc).\n>\n> Merge the two implementations into a single fsm-settings-unix.c shared\n> by both platforms.  The unified version uses the safer xstrdup() +\n> dirname() approach from the macOS implementation (avoiding strbuf\n> mutation with dirname()) and includes the \"vfat\" filesystem check.\n>\n> Update Makefile, meson.build, and CMakeLists.txt to use the new shared\n> file for non-Windows platforms.\n\nI guess exactly the same comment as the one for 05/10 applies here\nas well?\n\n"},{"id":"537149","messageId":"xmqqikbk8pqw.fsf@gitster.g","threadId":"64696","inReplyTo":"817489b3ea6a0831e4b815df1c4a0c0e100a2683.1772050636.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 09/10] run-command: add close_fd_above_stderr option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T21:41:59Z","receivedAt":"2026-02-25T21:42:01Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> Add a new option to struct child_process that closes file descriptors\n> 3 and above in the child after forking but before exec.  This prevents\n> long-running child processes from inheriting pipe endpoints or other\n> descriptors from the parent environment.\n>\n> The upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\n> capped at 4096 to avoid excessive iteration when the limit is set\n> very high.\n>\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  run-command.c | 11 +++++++++++\n>  run-command.h |  9 +++++++++\n>  2 files changed, 20 insertions(+)\n\nAll makes sense.\n\nI somehow find it a bit surprising that we did not already have this\nfeature anywhere, as closing all except for the low file descriptors\nconnected to stdio by default is fairly a common thing to do.\n\n\n> diff --git a/run-command.c b/run-command.c\n> index e3e02475cc..cbadcf5ff8 100644\n> --- a/run-command.c\n> +++ b/run-command.c\n> @@ -832,6 +832,17 @@ fail_pipe:\n>  \t\t\tchild_close(cmd->out);\n>  \t\t}\n>  \n> +\t\tif (cmd->close_fd_above_stderr) {\n> +\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n> +\t\t\tint fd;\n> +\t\t\tif (max_fd < 0 || max_fd > 4096)\n> +\t\t\t\tmax_fd = 4096;\n> +\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n> +\t\t\t\tif (fd != child_notifier)\n> +\t\t\t\t\tclose(fd);\n> +\t\t\t}\n> +\t\t}\n> +\n>  \t\tif (cmd->dir && chdir(cmd->dir))\n>  \t\t\tchild_die(CHILD_ERR_CHDIR);\n>  \n> diff --git a/run-command.h b/run-command.h\n> index 0df25e445f..a1aa1b1069 100644\n> --- a/run-command.h\n> +++ b/run-command.h\n> @@ -141,6 +141,15 @@ struct child_process {\n>  \tunsigned stdout_to_stderr:1;\n>  \tunsigned clean_on_exit:1;\n>  \tunsigned wait_after_clean:1;\n> +\n> +\t/**\n> +\t * Close file descriptors 3 and above in the child after forking\n> +\t * but before exec.  This prevents the long-running child from\n> +\t * inheriting pipe endpoints or other descriptors from the parent\n> +\t * environment (e.g., the test harness).\n> +\t */\n> +\tunsigned close_fd_above_stderr:1;\n> +\n>  \tvoid (*clean_on_exit_handler)(struct child_process *process);\n>  };\n"},{"id":"537161","messageId":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v6.git.git.1772050636.gitgitgadget@gmail.com","subject":"[PATCH v7 00/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:13Z","receivedAt":"2026-02-26T00:27:27Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" → \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (10):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: deduplicate IPC path logic for Unix platforms\n  fsmonitor: deduplicate settings logic for Unix platforms\n  fsmonitor: implement filesystem change listener for Linux\n  fsmonitor: add tests for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   4 +-\n builtin/fsmonitor--daemon.c                   |  71 +-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   4 +-\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 220 ++++++\n ...-settings-darwin.c => fsm-settings-unix.c} |  24 +-\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  29 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  11 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  | 101 ++-\n 18 files changed, 1278 insertions(+), 62 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (96%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (82%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v7\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v7\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v6:\n\n  1:  4d4dec8fa1 =  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  2:  d0bd3e32ca =  2:  d0bd3e32ca fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  3:  d2c5ca0939 =  3:  d2c5ca0939 compat/win32: add pthread_cond_timedwait\n  4:  0051a19303 !  4:  0a58670952 fsmonitor: use pthread_cond_timedwait for cookie wait\n     @@ builtin/fsmonitor--daemon.c: static enum fsmonitor_cookie_item_result with_lock_\n      -\t * this.  Also note that the above open() and unlink() calls\n      -\t * will cause at least two FS events on that path, so the odds\n      -\t * of getting stuck are pretty slim.\n     -+\t * Wait for the listener thread to see the cookie file.\n     ++\t * Wait for the listener thread to observe the cookie file.\n      +\t * Time out after a short interval so that the client\n      +\t * does not hang forever if the filesystem does not deliver\n      +\t * events (e.g., on certain container/overlay filesystems\n  5:  ff31e359a7 !  5:  037ae2a03f fsmonitor: deduplicate IPC path logic for Unix platforms\n     @@ Metadata\n       ## Commit message ##\n          fsmonitor: deduplicate IPC path logic for Unix platforms\n      \n     -    The IPC path logic for determining the Unix domain socket location is\n     -    nearly identical between macOS and Linux.  Both need to check whether\n     -    the .git directory is on a remote filesystem and, if so, fall back to\n     -    a socket path under $HOME or a user-configured directory.\n     +    The macOS fsm-ipc-darwin.c is applicable to other Unix variants as\n     +    well.  Rename it to fsm-ipc-unix.c and add a worktree NULL check\n     +    (BUG guard) that was missing from the macOS version.\n      \n     -    Merge the two implementations into a single fsm-ipc-unix.c that is\n     -    shared by both platforms.  The unified version includes the worktree\n     -    NULL check (BUG guard) from the Linux implementation, which was missing\n     -    in the macOS version.\n     -\n     -    Update Makefile, meson.build, and CMakeLists.txt to use the new shared\n     -    file for non-Windows platforms.\n     +    To support this, introduce FSMONITOR_OS_SETTINGS which is set to\n     +    \"unix\" for both macOS and Linux, distinct from FSMONITOR_DAEMON_BACKEND\n     +    which remains platform-specific (darwin, linux, win32).  Move\n     +    fsm-path-utils from FSMONITOR_OS_SETTINGS to FSMONITOR_DAEMON_BACKEND\n     +    since the path-utils files are platform-specific.\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n     @@ Makefile: ifdef FSMONITOR_DAEMON_BACKEND\n       endif\n       \n       ifdef FSMONITOR_OS_SETTINGS\n     + \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n     + \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n     +-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n     ++\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n     + endif\n     + \n     + ifdef WITH_BREAKING_CHANGES\n      \n       ## compat/fsmonitor/fsm-ipc-darwin.c => compat/fsmonitor/fsm-ipc-unix.c ##\n      @@ compat/fsmonitor/fsm-ipc-unix.c: const char *fsmonitor_ipc__get_path(struct repository *r)\n  6:  39da1e6be3 !  6:  0a83bb9c8e fsmonitor: deduplicate settings logic for Unix platforms\n     @@ Metadata\n       ## Commit message ##\n          fsmonitor: deduplicate settings logic for Unix platforms\n      \n     -    The fsmonitor settings logic for checking Unix domain socket\n     -    compatibility is nearly identical between macOS and Linux.  Both check\n     -    whether the IPC socket directory resides on a remote filesystem or a\n     -    filesystem that does not support Unix domain sockets (NTFS, FAT32, etc).\n     +    The macOS fsm-settings-darwin.c is applicable to other Unix variants\n     +    as well.  Rename it to fsm-settings-unix.c, using the safer\n     +    xstrdup()+dirname() approach and including the \"vfat\" filesystem check.\n      \n     -    Merge the two implementations into a single fsm-settings-unix.c shared\n     -    by both platforms.  The unified version uses the safer xstrdup() +\n     -    dirname() approach from the macOS implementation (avoiding strbuf\n     -    mutation with dirname()) and includes the \"vfat\" filesystem check.\n     -\n     -    Update Makefile, meson.build, and CMakeLists.txt to use the new shared\n     -    file for non-Windows platforms.\n     +    Now that both fsm-ipc and fsm-settings use the \"unix\" variant name,\n     +    set FSMONITOR_OS_SETTINGS to \"unix\" for macOS in config.mak.uname and\n     +    remove the if/else conditionals from the build files.\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## Makefile ##\n     -@@ Makefile: endif\n     +@@ Makefile: ifdef FSMONITOR_DAEMON_BACKEND\n     + \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n     + \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n     + \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n     +-ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n     +-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n     +-else\n     +-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n     +-endif\n     + endif\n       \n       ifdef FSMONITOR_OS_SETTINGS\n       \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n     --\tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n     -+ifeq ($(FSMONITOR_OS_SETTINGS),win32)\n     -+\tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-win32.o\n     -+else\n     -+\tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-unix.o\n     -+endif\n     - \tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n     ++\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n     + \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n     + \tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n       endif\n     - \n      \n       ## compat/fsmonitor/fsm-settings-darwin.c => compat/fsmonitor/fsm-settings-unix.c ##\n      @@\n     @@ compat/fsmonitor/fsm-settings-darwin.c => compat/fsmonitor/fsm-settings-unix.c\n       \t\treturn FSMONITOR_REASON_NOSOCKETS;\n       \t}\n      \n     + ## config.mak.uname ##\n     +@@ config.mak.uname: ifeq ($(uname_S),Darwin)\n     +         ifndef NO_PTHREADS\n     +         ifndef NO_UNIX_SOCKETS\n     + \tFSMONITOR_DAEMON_BACKEND = darwin\n     +-\tFSMONITOR_OS_SETTINGS = darwin\n     ++\tFSMONITOR_OS_SETTINGS = unix\n     +         endif\n     +         endif\n     + \n     +\n       ## contrib/buildsystems/CMakeLists.txt ##\n     -@@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n     +@@ contrib/buildsystems/CMakeLists.txt: endif()\n     + \n     + if(SUPPORTS_SIMPLE_IPC)\n     + \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n     +-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n     +-\n     +-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n     ++\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n     ++\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n     + \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n     ++\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n     ++\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n     ++\tendif()\n     ++\n     ++\tif(FSMONITOR_DAEMON_BACKEND)\n     + \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n       \n       \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n      -\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n       \tendif()\n       endif()\n       \n      \n       ## meson.build ##\n     +@@ meson.build: else\n     + endif\n     + \n     + fsmonitor_backend = ''\n     ++fsmonitor_os = ''\n     + if host_machine.system() == 'windows'\n     +   fsmonitor_backend = 'win32'\n     ++  fsmonitor_os = 'win32'\n     + elif host_machine.system() == 'darwin'\n     +   fsmonitor_backend = 'darwin'\n     ++  fsmonitor_os = 'unix'\n     +   libgit_dependencies += dependency('CoreServices')\n     + endif\n     + if fsmonitor_backend != ''\n      @@ meson.build: if fsmonitor_backend != ''\n           'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n           'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n           'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n      -    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n     ++    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     ++    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n         ]\n     - \n     -   if fsmonitor_backend == 'win32'\n     +-\n     +-  if fsmonitor_backend == 'win32'\n      -    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n     -+    libgit_sources += [\n     -+      'compat/fsmonitor/fsm-ipc-win32.c',\n     -+      'compat/fsmonitor/fsm-settings-win32.c',\n     -+    ]\n     -   else\n     +-  else\n      -    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n     -+    libgit_sources += [\n     -+      'compat/fsmonitor/fsm-ipc-unix.c',\n     -+      'compat/fsmonitor/fsm-settings-unix.c',\n     -+    ]\n     -   endif\n     +-  endif\n       endif\n       build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n     +-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n     ++build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n     + \n     + if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n     +   build_options_config.set('NO_REGEX', '')\n  7:  4eadc06004 !  7:  adac7964cc fsmonitor: implement filesystem change listener for Linux\n     @@ config.mak.uname: ifeq ($(uname_S),Linux)\n      +        ifndef NO_PTHREADS\n      +        ifndef NO_UNIX_SOCKETS\n      +\tFSMONITOR_DAEMON_BACKEND = linux\n     -+\tFSMONITOR_OS_SETTINGS = linux\n     ++\tFSMONITOR_OS_SETTINGS = unix\n      +\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n      +        endif\n      +        endif\n     @@ config.mak.uname: ifeq ($(uname_S),Linux)\n      \n       ## contrib/buildsystems/CMakeLists.txt ##\n      @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n     - \n     -+\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n     + \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n     + \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n     + \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n      +\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n     -+\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n     ++\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n     ++\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n      +\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)\n     -+\n     - \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)\n       \tendif()\n     + \n     + \tif(FSMONITOR_DAEMON_BACKEND)\n      \n       ## meson.build ##\n     -@@ meson.build: endif\n     - fsmonitor_backend = ''\n     +@@ meson.build: fsmonitor_os = ''\n       if host_machine.system() == 'windows'\n         fsmonitor_backend = 'win32'\n     +   fsmonitor_os = 'win32'\n      +elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n      +  fsmonitor_backend = 'linux'\n     ++  fsmonitor_os = 'unix'\n      +  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n       elif host_machine.system() == 'darwin'\n         fsmonitor_backend = 'darwin'\n     -   libgit_dependencies += dependency('CoreServices')\n     +   fsmonitor_os = 'unix'\n  8:  8fec92d5b4 =  8:  fad2f0a81a fsmonitor: add tests for Linux\n  9:  817489b3ea =  9:  c684fc9094 run-command: add close_fd_above_stderr option\n 10:  bb438afbbe = 10:  4987a009a2 fsmonitor: close inherited file descriptors and detach in daemon\n\n-- \ngitgitgadget\n"},{"id":"537162","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 01/10] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:14Z","receivedAt":"2026-02-26T00:27:29Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"537163","messageId":"d0bd3e32ca32b4150054ea91aa774a5b2db427e5.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 02/10] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:15Z","receivedAt":"2026-02-26T00:27:31Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  Add a hashmap_clear() call to prevent this\nmemory leak.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..4d52622e24 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\thashmap_clear(&state.cookies);\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"537164","messageId":"d2c5ca09396e020adb717055d82f50de7c1b7431.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 03/10] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:16Z","receivedAt":"2026-02-26T00:27:32Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nThis enables callers to use bounded waits on condition variables\ninstead of blocking indefinitely with pthread_cond_wait().\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..538ef92d9d 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\ttimeout_ms = 0;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"537165","messageId":"0a586709524f36c189cc32159b643a49abdbd51c.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:17Z","receivedAt":"2026-02-26T00:27:34Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 4d52622e24..110fe5fb55 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"537166","messageId":"037ae2a03f8576a73feb889cd61220ba69d97fea.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 05/10] fsmonitor: deduplicate IPC path logic for Unix platforms","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:18Z","receivedAt":"2026-02-26T00:27:35Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe macOS fsm-ipc-darwin.c is applicable to other Unix variants as\nwell.  Rename it to fsm-ipc-unix.c and add a worktree NULL check\n(BUG guard) that was missing from the macOS version.\n\nTo support this, introduce FSMONITOR_OS_SETTINGS which is set to\n\"unix\" for both macOS and Linux, distinct from FSMONITOR_DAEMON_BACKEND\nwhich remains platform-specific (darwin, linux, win32).  Move\nfsm-path-utils from FSMONITOR_OS_SETTINGS to FSMONITOR_DAEMON_BACKEND\nsince the path-utils files are platform-specific.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                              | 8 ++++++--\n compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} | 4 +++-\n contrib/buildsystems/CMakeLists.txt                   | 2 +-\n meson.build                                           | 7 ++++++-\n 4 files changed, 16 insertions(+), 5 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (96%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..7480ce3e1d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2365,13 +2365,17 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n+ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n+else\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n+endif\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 96%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\nindex fe149a1b37..d34a6419bc 100644\n--- a/compat/fsmonitor/fsm-ipc-darwin.c\n+++ b/compat/fsmonitor/fsm-ipc-unix.c\n@@ -27,13 +27,15 @@ const char *fsmonitor_ipc__get_path(struct repository *r)\n \tif (ipc_path)\n \t\treturn ipc_path;\n \n-\n \t/* By default the socket file is created in the .git directory */\n \tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n \t\tipc_path = fsmonitor_ipc__get_default_path();\n \t\treturn ipc_path;\n \t}\n \n+\tif (!r->worktree)\n+\t\tBUG(\"repository has no worktree\");\n+\n \tgit_SHA1_Init(&sha1ctx);\n \tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n \tgit_SHA1_Final(hash, &sha1ctx);\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..32ef6ebe1b 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -303,7 +303,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..8de795f9d4 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1332,11 +1332,16 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n+\n+  if fsmonitor_backend == 'win32'\n+    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n+  else\n+    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n+  endif\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537167","messageId":"0a83bb9c8e71f6c388a50eb62afd03680020eb94.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 06/10] fsmonitor: deduplicate settings logic for Unix platforms","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:19Z","receivedAt":"2026-02-26T00:27:37Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe macOS fsm-settings-darwin.c is applicable to other Unix variants\nas well.  Rename it to fsm-settings-unix.c, using the safer\nxstrdup()+dirname() approach and including the \"vfat\" filesystem check.\n\nNow that both fsm-ipc and fsm-settings use the \"unix\" variant name,\nset FSMONITOR_OS_SETTINGS to \"unix\" for macOS in config.mak.uname and\nremove the if/else conditionals from the build files.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 +----\n ...-settings-darwin.c => fsm-settings-unix.c} | 24 +++++++++++-------\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   | 14 +++++------\n 5 files changed, 35 insertions(+), 36 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (82%)\n\ndiff --git a/Makefile b/Makefile\nindex 7480ce3e1d..062347997a 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2365,15 +2365,11 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n-else\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n-endif\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 82%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\nindex a382590635..27d89207af 100644\n--- a/compat/fsmonitor/fsm-settings-darwin.c\n+++ b/compat/fsmonitor/fsm-settings-unix.c\n@@ -5,7 +5,7 @@\n #include \"fsmonitor-settings.h\"\n #include \"fsmonitor-path-utils.h\"\n \n- /*\n+/*\n  * For the builtin FSMonitor, we create the Unix domain socket for the\n  * IPC in the .git directory.  If the working directory is remote,\n  * then the socket will be created on the remote file system.  This\n@@ -22,25 +22,31 @@\n  * The builtin FSMonitor uses a Unix domain socket in the .git\n  * directory for IPC.  These Windows drive formats do not support\n  * Unix domain sockets, so mark them as incompatible for the daemon.\n- *\n  */\n static enum fsmonitor_reason check_uds_volume(struct repository *r)\n {\n \tstruct fs_info fs;\n \tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n-\tstruct strbuf path = STRBUF_INIT;\n-\tstrbuf_add(&path, ipc_path, strlen(ipc_path));\n+\tchar *path;\n+\tchar *dir;\n+\n+\t/*\n+\t * Create a copy for dirname() since it may modify its argument.\n+\t */\n+\tpath = xstrdup(ipc_path);\n+\tdir = dirname(path);\n \n-\tif (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {\n-\t\tstrbuf_release(&path);\n+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n+\t\tfree(path);\n \t\treturn FSMONITOR_REASON_ERROR;\n \t}\n \n-\tstrbuf_release(&path);\n+\tfree(path);\n \n \tif (fs.is_remote ||\n-\t\t!strcmp(fs.typename, \"msdos\") ||\n-\t\t!strcmp(fs.typename, \"ntfs\")) {\n+\t    !strcmp(fs.typename, \"msdos\") ||\n+\t    !strcmp(fs.typename, \"ntfs\") ||\n+\t    !strcmp(fs.typename, \"vfat\")) {\n \t\tfree(fs.typename);\n \t\treturn FSMONITOR_REASON_NOSOCKETS;\n \t}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 32ef6ebe1b..4099f9a951 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 8de795f9d4..589624f399 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1334,17 +1337,12 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n-\n-  if fsmonitor_backend == 'win32'\n-    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n-  else\n-    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n-  endif\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"537168","messageId":"adac7964cce28d237ef09574858be240fe3269fc.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 07/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:20Z","receivedAt":"2026-02-26T00:27:39Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 220 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   4 +\n meson.build                                 |   4 +\n 8 files changed, 1043 insertions(+), 6 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..36a701c06a\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..b4c19e0655\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,220 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef V9FS_MAGIC\n+#define V9FS_MAGIC 0x01021997\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Get the filesystem type name for logging purposes.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase V9FS_MAGIC:\n+\t\treturn \"9p\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ * Returns the filesystem type for the longest matching mount point.\n+ */\n+static char *find_mount(const char *path, struct statfs *fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\tstruct statfs path_fs;\n+\n+\tif (statfs(path, &path_fs) < 0)\n+\t\treturn NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t\t*fs = mount_fs;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 4099f9a951..00d6e757b6 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\ndiff --git a/meson.build b/meson.build\nindex 589624f399..0b033a9b9a 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"537169","messageId":"fad2f0a81ab5fcdced2cdf3c02877fbb00244efa.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 08/10] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:21Z","receivedAt":"2026-02-26T00:27:40Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/meson.build                |  8 +++-\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 2 files changed, 89 insertions(+), 8 deletions(-)\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 19e8306298..85ef2ae2fa 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1210,12 +1210,18 @@ test_environment = script_environment\n test_environment.set('GIT_BUILD_DIR', git_build_dir)\n \n foreach integration_test : integration_tests\n+  per_test_kwargs = test_kwargs\n+  # The fsmonitor tests start daemon processes that in some environments\n+  # can hang.  Set a generous timeout to prevent CI from blocking.\n+  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n+    per_test_kwargs += {'timeout': 1800}\n+  endif\n   test(fs.stem(integration_test), shell,\n     args: [ integration_test ],\n     workdir: meson.current_source_dir(),\n     env: test_environment,\n     depends: test_dependencies + bin_wrappers,\n-    kwargs: test_kwargs,\n+    kwargs: per_test_kwargs,\n   )\n endforeach\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"537170","messageId":"c684fc90946d2944e1f6f9f09a882fcbc2b7b393.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 09/10] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:22Z","receivedAt":"2026-02-26T00:27:42Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a new option to struct child_process that closes file descriptors\n3 and above in the child after forking but before exec.  This prevents\nlong-running child processes from inheriting pipe endpoints or other\ndescriptors from the parent environment.\n\nThe upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\ncapped at 4096 to avoid excessive iteration when the limit is set\nvery high.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 11 +++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 20 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..cbadcf5ff8 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -832,6 +832,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..a1aa1b1069 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the long-running child from\n+\t * inheriting pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"537171","messageId":"4987a009a2e74a04aa1a8deb0508c971a3df0549.1772065643.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v7 10/10] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T00:27:23Z","receivedAt":"2026-02-26T00:27:43Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at daemon startup so\nthat file descriptors 3 and above are closed before any daemon work\nbegins.  This ensures the daemon does not inadvertently hold open\ndescriptors from its launching environment.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  Without this, shells that\nenable job control (e.g. bash with \"set -m\") treat the daemon as part\nof the spawning command's job.  Their \"wait\" builtin then blocks until\nthe daemon exits, which it never does.  This specifically affects\nsystems where /bin/sh is bash (e.g. Fedora), since dash only waits for\nthe specific PID rather than the full process group.\n\nAdd a 30-second timeout to \"fsmonitor--daemon stop\" so it does\nnot block indefinitely if the daemon fails to shut down.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c  | 28 +++++++++++++++++++++++++---\n fsmonitor-ipc.c              |  3 +++\n t/meson.build                |  8 +-------\n t/t7527-builtin-fsmonitor.sh | 12 +++++++++++-\n 4 files changed, 40 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 110fe5fb55..be8f3fee1a 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n@@ -1431,7 +1441,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1451,10 +1461,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1517,6 +1538,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 85ef2ae2fa..19e8306298 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1210,18 +1210,12 @@ test_environment = script_environment\n test_environment.set('GIT_BUILD_DIR', git_build_dir)\n \n foreach integration_test : integration_tests\n-  per_test_kwargs = test_kwargs\n-  # The fsmonitor tests start daemon processes that in some environments\n-  # can hang.  Set a generous timeout to prevent CI from blocking.\n-  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n-    per_test_kwargs += {'timeout': 1800}\n-  endif\n   test(fs.stem(integration_test), shell,\n     args: [ integration_test ],\n     workdir: meson.current_source_dir(),\n     env: test_environment,\n     depends: test_dependencies + bin_wrappers,\n-    kwargs: per_test_kwargs,\n+    kwargs: test_kwargs,\n   )\n endforeach\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 774da5ac60..d7e64bcb7a 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -766,7 +766,7 @@ do\n \t\telse\n \t\t\ttest_expect_success \"Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor\" '\n \t\t\t\tgit config core.fsmonitor true &&\n-\t\t\t\tgit fsmonitor--daemon start &&\n+\t\t\t\tgit fsmonitor--daemon start --start-timeout=10 &&\n \t\t\t\tgit update-index --fsmonitor\n \t\t\t'\n \t\tfi\n@@ -997,7 +997,17 @@ start_git_in_background () {\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n \tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n+\n+\t# Disable job control before wait.  With \"set -m\", bash treats\n+\t# \"wait $pid\" as waiting for the entire job (process group),\n+\t# which blocks indefinitely if the fsmonitor daemon was spawned\n+\t# into the same process group and is still running.  Turning off\n+\t# job control makes \"wait\" only wait for the specific PID.\n+\tset +m &&\n \twait $git_pid\n+\twait_status=$?\n+\tset -m\n+\treturn $wait_status\n }\n \n stop_git () {\n-- \ngitgitgadget\n"},{"id":"537200","messageId":"xmqq5x7j5xje.fsf@gitster.g","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 00/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-26T15:34:13Z","receivedAt":"2026-02-26T15:34:16Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Changes since v6:\n>\n>  * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n>    and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n>    Makefile, meson.build, and CMakeLists.txt per Junio's review\n>  * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n>    FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n>  * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n>    VM/container host mounts where fsmonitor works fine)\n>  * Removed redundant #include <libgen.h> (already provided by\n>    compat/posix.h)\n>  * Fixed cookie wait comment wording (\"see\" → \"observe\")\n>  * Rewrote commit messages for IPC and settings dedup patches\n\nI saw nothing unexpected in this iteration relative to the previous\none.  Looking good.\n\nIt would be nice to have another set of eyes on the inotify part of\nthis series before we mark it for 'next'.\n\nThanks.\n"},{"id":"537272","messageId":"20260227063118.9069-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqzf4w8r20.fsf@gitster.g","subject":"Re: [PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-27T06:31:18Z","receivedAt":"2026-02-27T06:31:21Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I cannot convince myself if one-second interval is not too frequent\n> to force everybody, including those with working inotify, to poll.\n> I wonder if this is something that may want to be configurable (or\n> better yet, auto-detectable, but that may be wishing for moon).\n\nThe 1-second timeout only fires when the filesystem fails to deliver\nthe cookie event at all (e.g. overlayfs in containers where inotify\nwatches succeed but events never arrive). On a working filesystem\nthe cookie event comes back in well under a millisecond, so the\ntimeout never triggers. When it does fire, the client falls back to\na full scan, which is the safe default. Happy to make it\nconfigurable if you think that's worth it, but the current behavior\nseemed reasonable as a starting point.\n\nThanks,\nPaul\n"},{"id":"537273","messageId":"20260227063125.9111-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqv7fk8qvq.fsf@gitster.g","subject":"Re: [PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-27T06:31:25Z","receivedAt":"2026-02-27T06:31:27Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Is this a complete sentence?  Or perhaps something like \"see\" ->\n> \"check\"?\n\nFixed, changed to \"observe\".\n\nThanks,\nPaul\n"},{"id":"537274","messageId":"20260227063128.9135-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqqzq88q9u.fsf@gitster.g","subject":"Re: [PATCH v6 05/10] fsmonitor: deduplicate IPC path logic for Unix platforms","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-27T06:31:28Z","receivedAt":"2026-02-27T06:31:30Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> But the patch seems to indicate somewhat different story.  The code\n> before this patch started with a single macOS (darwin) one, but\n> because it is mostly applicable to other UNIX variants as well, the\n> patch renames the existing macOS one for unix and makes a small\n> adjustment (namely, asserts that r->worktree is not NULL).\n\nGood point. Restructured: FSMONITOR_OS_SETTINGS is now set to \"unix\"\nfor both macOS and Linux (and \"win32\" for Windows). The build files\nuse $(FSMONITOR_OS_SETTINGS) for fsm-ipc and fsm-settings, and\n$(FSMONITOR_DAEMON_BACKEND) for the platform-specific files (listen,\nhealth, path-utils). No more if/else blocks.\n\nAlso rewrote both commit messages to describe what's actually\nhappening (renaming darwin to unix) rather than \"merging two\nimplementations\".\n\nThanks,\nPaul\n"},{"id":"537275","messageId":"20260227063130.9156-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqms0w8q8p.fsf@gitster.g","subject":"Re: [PATCH v6 06/10] fsmonitor: deduplicate settings logic for Unix platforms","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-27T06:31:30Z","receivedAt":"2026-02-27T06:31:32Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I guess exactly the same comment as the one for 05/10 applies here\n> as well?\n\nYep, same fix applied here. Also rewrote both commit messages to\ndescribe what's actually happening (renaming darwin to unix) rather\nthan \"merging two implementations\".\n\nThanks,\nPaul\n"},{"id":"537301","messageId":"xmqqfr6mt9uk.fsf@gitster.g","threadId":"64696","inReplyTo":"20260227063118.9069-1-github@paulisageek.com","subject":"Re: [PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-27T16:44:19Z","receivedAt":"2026-02-27T16:44:22Z","isPatch":true,"body":"Paul Tarjan <paul@paultarjan.com> writes:\n\n> The 1-second timeout only fires when the filesystem fails to deliver\n> the cookie event at all (e.g. overlayfs in containers where inotify\n> watches succeed but events never arrive). On a working filesystem\n> the cookie event comes back in well under a millisecond, so the\n> timeout never triggers.\n\nI am not worried about that case.  When the filesystem is quiescent\nand there is absolutely nothing fsmonitor needs to report, wouldn't\nwe see no \"cookie event\" delivered at all?\n\n> When it does fire, the client falls back to\n> a full scan, which is the safe default.\n\nAnd if a every-one-second timeout forces somebody to fall back to a\nfull scan every second, that does not sound like a safe default to\nme.\n\nI am clearly missing something here.  Are we handling two different\nkind of events, one that wakes us up to expect \"cookie\" events, and\nthe other \"cookie\" events, and we know the delivery of the former is\nreliable while the latter not?  So on a quiescent filesystem we do\nnot even get the first kind of event to wake us up, and we do not\nstart waiting for \"cookie\" events with 1-sec timeout in the first\nplace?  If so, that does sound like a good arrangement.\n\n"},{"id":"537367","messageId":"20260228002830.42676-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqfr6mt9uk.fsf@gitster.g","subject":"Re: [PATCH v6 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-02-28T00:28:30Z","receivedAt":"2026-02-28T00:28:33Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I am clearly missing something here.  Are we handling two different\n> kind of events, one that wakes us up to expect \"cookie\" events, and\n> the other \"cookie\" events, and we know the delivery of the former is\n> reliable while the latter not?  So on a quiescent filesystem we do\n> not even get the first kind of event to wake us up, and we do not\n> start waiting for \"cookie\" events with 1-sec timeout in the first\n> place?  If so, that does sound like a good arrangement.\n\nYes, that's exactly right. The cookie wait only runs when a client\nconnects and asks for the current status. The daemon creates a\ntemporary cookie file, then waits for the listener thread to see the\ninotify event for that file. On a quiescent filesystem with no\nclients asking, this code path never executes and the timeout never\nfires.\n\nSo the only time the 1-second timeout can trigger is when a client\nis actively waiting for a response and the filesystem isn't\ndelivering events at all. In that case falling back to a full scan\nis the right thing to do since we can't trust the event stream\nanyway.\n\nThanks,\nPaul\n"},{"id":"537755","messageId":"aafiaS-DscdIkori@pks.im","threadId":"64696","inReplyTo":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 01/10] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:42:33Z","receivedAt":"2026-03-04T07:42:44Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:14AM +0000, Paul Tarjan via GitGitGadget wrote:\n> diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\n> index 242c594646..bc4571938c 100644\n> --- a/builtin/fsmonitor--daemon.c\n> +++ b/builtin/fsmonitor--daemon.c\n> @@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \tconst struct fsmonitor_batch *batch;\n>  \tstruct fsmonitor_batch *remainder = NULL;\n>  \tintmax_t count = 0, duplicates = 0;\n> -\tkh_str_t *shown;\n> +\tkh_str_t *shown = NULL;\n>  \tint hash_ret;\n>  \tint do_trivial = 0;\n>  \tint do_flush = 0;\n> @@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \t\ttotal_response_len += payload.len;\n>  \t}\n>  \n> -\tkh_release_str(shown);\n> -\n>  \tpthread_mutex_lock(&state->main_lock);\n>  \n>  \tif (token_data->client_ref_count > 0)\n> @@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n>  \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n>  \n>  cleanup:\n> +\tkh_destroy_str(shown);\n>  \tstrbuf_release(&response_token);\n>  \tstrbuf_release(&requested_token_id);\n>  \tstrbuf_release(&payload);\n\nMakes sense. If I understood correctly I think we could improve this\ncode to stop using khash directly and instead use a strmap, which\nhas a nicer interface. But that's certainly outside of the scope of this\npatch series and rather a #leftoverbit.\n\nPatrick\n"},{"id":"537756","messageId":"aaficy025yUqsgN5@pks.im","threadId":"64696","inReplyTo":"d0bd3e32ca32b4150054ea91aa774a5b2db427e5.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 02/10] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:42:43Z","receivedAt":"2026-03-04T07:42:47Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:15AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> The `state.cookies` hashmap is initialized during daemon startup but\n> never freed during cleanup in the `done:` label of\n> fsmonitor_run_daemon().  Add a hashmap_clear() call to prevent this\n> memory leak.\n> \n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  builtin/fsmonitor--daemon.c | 1 +\n>  1 file changed, 1 insertion(+)\n> \n> diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\n> index bc4571938c..4d52622e24 100644\n> --- a/builtin/fsmonitor--daemon.c\n> +++ b/builtin/fsmonitor--daemon.c\n> @@ -1404,6 +1404,7 @@ static int fsmonitor_run_daemon(void)\n>  done:\n>  \tpthread_cond_destroy(&state.cookies_cond);\n>  \tpthread_mutex_destroy(&state.main_lock);\n> +\thashmap_clear(&state.cookies);\n>  \tfsm_listen__dtor(&state);\n>  \tfsm_health__dtor(&state);\n\nIs this actually sufficient? as far as I can see, the cookies are\ninserted in `__wait_for_cookie()`, and each cookie also has a name\nattached to it that was allocated via a strbuf. So don't we have to\nfree the name, as well?\n\nPatrick\n"},{"id":"537757","messageId":"aafieD42pMaYsnRw@pks.im","threadId":"64696","inReplyTo":"d2c5ca09396e020adb717055d82f50de7c1b7431.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 03/10] compat/win32: add pthread_cond_timedwait","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:42:48Z","receivedAt":"2026-03-04T07:42:52Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:16AM +0000, Paul Tarjan via GitGitGadget wrote:\n> diff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\n> index 7e93146963..538ef92d9d 100644\n> --- a/compat/win32/pthread.c\n> +++ b/compat/win32/pthread.c\n> @@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n>  \t\treturn err_win_to_posix(GetLastError());\n>  \treturn 0;\n>  }\n> +\n> +int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n> +\t\t\t   const struct timespec *abstime)\n> +{\n> +\tstruct timeval now;\n> +\tlong long now_ms, deadline_ms;\n> +\tDWORD timeout_ms;\n> +\n> +\tgettimeofday(&now, NULL);\n> +\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n> +\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n> +\t\t      abstime->tv_nsec / 1000000;\n> +\n> +\tif (deadline_ms <= now_ms)\n> +\t\ttimeout_ms = 0;\n\nAccording to pthread_cond_timedwait(3p) we should return an error in\nthas case:\n\n  The  pthread_cond_timedwait()  function  shall be equivalent to\n  pthread_cond_wait(), except that an error is returned if the absolute\n  time specified by abstime passes (that is, system time equals or\n  exceeds abstime) before the condition cond is signaled or broadcasted,\n  or if the absolute  time  specified  by abstime  has  already  been\n  passed at the time of the call.\n\nSo I guess it's safe to return ETIMEDOUT directly here?\n\n> +\telse\n> +\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n> +\n> +\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n\nThe function returns a BOOL, so comparing with `== 0` is misleading. I\nsee that this is following the pattern of `pthread_cond_wait()` though,\nso I guess it's okayish.\n\n> +\t\tDWORD err = GetLastError();\n> +\t\tif (err == ERROR_TIMEOUT)\n> +\t\t\treturn ETIMEDOUT;\n> +\t\treturn err_win_to_posix(err);\n\nWouldn't it make sense to extend `err_win_to_posix()` instead?\n\nPatrick\n"},{"id":"537758","messageId":"aafifU-befdZW4O0@pks.im","threadId":"64696","inReplyTo":"0a586709524f36c189cc32159b643a49abdbd51c.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:42:53Z","receivedAt":"2026-03-04T07:42:57Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:17AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> The cookie wait in with_lock__wait_for_cookie() uses an infinite\n> pthread_cond_wait() loop.  The existing comment notes the desire\n> to switch to pthread_cond_timedwait(), but the routine was not\n> available in git thread-utils.\n> \n> On certain container or overlay filesystems, inotify watches may\n> succeed but events are never delivered.  In this case the daemon\n> would hang indefinitely waiting for the cookie event, which in\n> turn causes the client to hang.\n> \n> Replace the infinite wait with a one-second timeout using\n> pthread_cond_timedwait().  If the timeout fires, report an\n> error and let the client proceed with a trivial (full-scan)\n> response rather than blocking forever.\n\nOne thing that I'd be happy to learn about is why specifically you have\nchosen one second as a timeout value. Are we sure this is always enough\non a loaded system?\n\nPatrick\n"},{"id":"537759","messageId":"aafigc1QiysIByhM@pks.im","threadId":"64696","inReplyTo":"037ae2a03f8576a73feb889cd61220ba69d97fea.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 05/10] fsmonitor: deduplicate IPC path logic for Unix platforms","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:42:57Z","receivedAt":"2026-03-04T07:43:02Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:18AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n\nNit: we're not exactly deduplicating just yet, but are rather preparing\nfor that as there is no second implementation using this yet.\n\n> diff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\n> similarity index 96%\n> rename from compat/fsmonitor/fsm-ipc-darwin.c\n> rename to compat/fsmonitor/fsm-ipc-unix.c\n> index fe149a1b37..d34a6419bc 100644\n> --- a/compat/fsmonitor/fsm-ipc-darwin.c\n> +++ b/compat/fsmonitor/fsm-ipc-unix.c\n> @@ -27,13 +27,15 @@ const char *fsmonitor_ipc__get_path(struct repository *r)\n>  \tif (ipc_path)\n>  \t\treturn ipc_path;\n>  \n> -\n>  \t/* By default the socket file is created in the .git directory */\n>  \tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n>  \t\tipc_path = fsmonitor_ipc__get_default_path();\n>  \t\treturn ipc_path;\n>  \t}\n>  \n> +\tif (!r->worktree)\n> +\t\tBUG(\"repository has no worktree\");\n> +\n>  \tgit_SHA1_Init(&sha1ctx);\n>  \tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n>  \tgit_SHA1_Final(hash, &sha1ctx);\n\nI think these while-at-it changes should be removed from this commit.\n\nPatrick\n"},{"id":"537760","messageId":"aafihm4MVoVOaD2l@pks.im","threadId":"64696","inReplyTo":"0a83bb9c8e71f6c388a50eb62afd03680020eb94.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 06/10] fsmonitor: deduplicate settings logic for Unix platforms","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:43:02Z","receivedAt":"2026-03-04T07:43:07Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:19AM +0000, Paul Tarjan via GitGitGadget wrote:\n> diff --git a/Makefile b/Makefile\n> index 7480ce3e1d..062347997a 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -2365,15 +2365,11 @@ ifdef FSMONITOR_DAEMON_BACKEND\n>  \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n>  \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n>  \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n> -ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n> -\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n> -else\n> -\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n> -endif\n>  endif\n>  \n>  ifdef FSMONITOR_OS_SETTINGS\n>  \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n> +\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n>  \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n>  \tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n>  endif\n\nIt's a bit weird that the only change to the Makefile here is to change\nhow we wire up fsm-ipc even though it's fsm-settings that this commit\ncares about. Should this change be moved into the preceding commit?\n\n> diff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\n> similarity index 82%\n> rename from compat/fsmonitor/fsm-settings-darwin.c\n> rename to compat/fsmonitor/fsm-settings-unix.c\n> index a382590635..27d89207af 100644\n> --- a/compat/fsmonitor/fsm-settings-darwin.c\n> +++ b/compat/fsmonitor/fsm-settings-unix.c\n> @@ -5,7 +5,7 @@\n>  #include \"fsmonitor-settings.h\"\n>  #include \"fsmonitor-path-utils.h\"\n>  \n> - /*\n> +/*\n>   * For the builtin FSMonitor, we create the Unix domain socket for the\n>   * IPC in the .git directory.  If the working directory is remote,\n>   * then the socket will be created on the remote file system.  This\n> @@ -22,25 +22,31 @@\n>   * The builtin FSMonitor uses a Unix domain socket in the .git\n>   * directory for IPC.  These Windows drive formats do not support\n>   * Unix domain sockets, so mark them as incompatible for the daemon.\n> - *\n>   */\n>  static enum fsmonitor_reason check_uds_volume(struct repository *r)\n>  {\n>  \tstruct fs_info fs;\n>  \tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\tstrbuf_add(&path, ipc_path, strlen(ipc_path));\n> +\tchar *path;\n> +\tchar *dir;\n> +\n> +\t/*\n> +\t * Create a copy for dirname() since it may modify its argument.\n> +\t */\n> +\tpath = xstrdup(ipc_path);\n> +\tdir = dirname(path);\n>  \n> -\tif (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {\n> -\t\tstrbuf_release(&path);\n> +\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n> +\t\tfree(path);\n>  \t\treturn FSMONITOR_REASON_ERROR;\n>  \t}\n>  \n> -\tstrbuf_release(&path);\n> +\tfree(path);\n>  \n>  \tif (fs.is_remote ||\n> -\t\t!strcmp(fs.typename, \"msdos\") ||\n> -\t\t!strcmp(fs.typename, \"ntfs\")) {\n> +\t    !strcmp(fs.typename, \"msdos\") ||\n> +\t    !strcmp(fs.typename, \"ntfs\") ||\n> +\t    !strcmp(fs.typename, \"vfat\")) {\n>  \t\tfree(fs.typename);\n>  \t\treturn FSMONITOR_REASON_NOSOCKETS;\n>  \t}\n\nSame here, it's not clear to me where those while-at-it changes are\ncoming from and whether we need them here. I'd rather drop them.\n\n> diff --git a/meson.build b/meson.build\n> index 8de795f9d4..589624f399 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -1320,10 +1320,13 @@ else\n>  endif\n>  \n>  fsmonitor_backend = ''\n> +fsmonitor_os = ''\n>  if host_machine.system() == 'windows'\n>    fsmonitor_backend = 'win32'\n> +  fsmonitor_os = 'win32'\n>  elif host_machine.system() == 'darwin'\n>    fsmonitor_backend = 'darwin'\n> +  fsmonitor_os = 'unix'\n>    libgit_dependencies += dependency('CoreServices')\n>  endif\n>  if fsmonitor_backend != ''\n\nI think it might make sense to introduce the `fsmonitor_os` variable in\nthe preceding commit already. If so...\n\n> @@ -1334,17 +1337,12 @@ if fsmonitor_backend != ''\n>      'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n>      'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n>      'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n> -    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n> +    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n> +    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n>    ]\n> -\n> -  if fsmonitor_backend == 'win32'\n> -    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n> -  else\n> -    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n> -  endif\n>  endif\n\nWe could avoid the flip-flopping of the code here.\n\nPatrick\n"},{"id":"537761","messageId":"aafii97SoHS96rkA@pks.im","threadId":"64696","inReplyTo":"adac7964cce28d237ef09574858be240fe3269fc.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 07/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:43:07Z","receivedAt":"2026-03-04T07:43:12Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:20AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> Implement the built-in fsmonitor daemon for Linux using the inotify\n> API, bringing it to feature parity with the existing Windows and macOS\n> implementations.\n> \n> The implementation uses inotify rather than fanotify because fanotify\n> requires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\n> unsuitable for an unprivileged user-space daemon.  While inotify has\n> the limitation of requiring a separate watch on every directory (unlike\n> macOS's FSEvents, which can monitor an entire directory tree with a\n> single watch), it operates without elevated privileges and provides\n> the per-file event granularity needed for fsmonitor.\n\nThanks for adding this explanation, makes sense.\n\n> diff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\n> new file mode 100644\n> index 0000000000..b4c19e0655\n> --- /dev/null\n> +++ b/compat/fsmonitor/fsm-path-utils-linux.c\n> @@ -0,0 +1,220 @@\n> +#include \"git-compat-util.h\"\n> +#include \"fsmonitor-ll.h\"\n> +#include \"fsmonitor-path-utils.h\"\n> +#include \"gettext.h\"\n> +#include \"trace.h\"\n> +\n> +#include <sys/statfs.h>\n> +\n> +#ifdef HAVE_LINUX_MAGIC_H\n\nI saw that this define is only wired up for CMake. I guess we should\nalso add it to our Makefile (probably via config.mak.uname) and Meson\n(probably via compiler.has_header()).\n\n[snip]\n> +/*\n> + * Get the filesystem type name for logging purposes.\n> + */\n> +static const char *get_fs_typename(unsigned long f_type)\n> +{\n> +\tswitch (f_type) {\n> +\tcase CIFS_SUPER_MAGIC:\n> +\t\treturn \"cifs\";\n> +\tcase SMB_SUPER_MAGIC:\n> +\t\treturn \"smb\";\n> +\tcase SMB2_SUPER_MAGIC:\n> +\t\treturn \"smb2\";\n> +\tcase NFS_SUPER_MAGIC:\n> +\t\treturn \"nfs\";\n> +\tcase AFS_SUPER_MAGIC:\n> +\t\treturn \"afs\";\n> +\tcase CODA_SUPER_MAGIC:\n> +\t\treturn \"coda\";\n> +\tcase V9FS_MAGIC:\n> +\t\treturn \"9p\";\n> +\tcase FUSE_SUPER_MAGIC:\n> +\t\treturn \"fuse\";\n> +\tdefault:\n> +\t\treturn \"unknown\";\n> +\t}\n> +}\n\nThis selection looks rather interesting to me. Why wouldn't we include\ncommon filesystems like ext4 and the like? Certainly hints that the\nfunction needs better documentation, and potentially a better name.\n\n> +/*\n> + * Find the mount point for a given path by reading /proc/mounts.\n> + * Returns the filesystem type for the longest matching mount point.\n> + */\n> +static char *find_mount(const char *path, struct statfs *fs)\n> +{\n> +\tFILE *fp;\n> +\tstruct strbuf line = STRBUF_INIT;\n> +\tstruct strbuf match = STRBUF_INIT;\n> +\tstruct strbuf fstype = STRBUF_INIT;\n> +\tchar *result = NULL;\n> +\tstruct statfs path_fs;\n> +\n> +\tif (statfs(path, &path_fs) < 0)\n> +\t\treturn NULL;\n> +\n> +\tfp = fopen(\"/proc/mounts\", \"r\");\n> +\tif (!fp)\n> +\t\treturn NULL;\n> +\n> +\twhile (strbuf_getline(&line, fp) != EOF) {\n> +\t\tchar *fields[6];\n> +\t\tchar *p = line.buf;\n> +\t\tint i;\n> +\n> +\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n> +\t\tfor (i = 0; i < 6 && p; i++) {\n> +\t\t\tfields[i] = p;\n> +\t\t\tp = strchr(p, ' ');\n> +\t\t\tif (p)\n> +\t\t\t\t*p++ = '\\0';\n> +\t\t}\n> +\n> +\t\tif (i >= 3) {\n> +\t\t\tconst char *mountpoint = fields[1];\n> +\t\t\tconst char *type = fields[2];\n> +\t\t\tstruct statfs mount_fs;\n> +\n> +\t\t\t/* Check if this mount point is a prefix of our path */\n> +\t\t\tif (starts_with(path, mountpoint) &&\n> +\t\t\t    (path[strlen(mountpoint)] == '/' ||\n> +\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n> +\t\t\t\t/* Check if filesystem ID matches */\n> +\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n> +\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n> +\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n> +\t\t\t\t\t/* Keep the longest matching mount point */\n> +\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n> +\t\t\t\t\t\tstrbuf_reset(&match);\n> +\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n> +\t\t\t\t\t\tstrbuf_reset(&fstype);\n> +\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n> +\t\t\t\t\t\t*fs = mount_fs;\n> +\t\t\t\t\t}\n> +\t\t\t\t}\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\n> +\tfclose(fp);\n> +\tstrbuf_release(&line);\n> +\tstrbuf_release(&match);\n> +\n> +\tif (fstype.len)\n> +\t\tresult = strbuf_detach(&fstype, NULL);\n> +\telse\n> +\t\tstrbuf_release(&fstype);\n> +\n> +\treturn result;\n> +}\n\nSorry, but I still don't quite understand what we're doing here. Isn't\nthe longest matching mount point always the one that statfs(3p) gave us?\nWhy do we have to scan \"/proc/mounts\"?\n\nPatrick\n"},{"id":"537762","messageId":"aafikA4bQS3lB0Hq@pks.im","threadId":"64696","inReplyTo":"fad2f0a81ab5fcdced2cdf3c02877fbb00244efa.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 08/10] fsmonitor: add tests for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:43:12Z","receivedAt":"2026-03-04T07:43:17Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:21AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> Add a smoke test that verifies the filesystem actually delivers\n> inotify events to the daemon.  On some configurations (e.g.,\n> overlayfs with older kernels), inotify watches succeed but events\n> are never delivered.  The daemon cookie wait will time out, but\n> every subsequent test would fail.  Skip the entire test file early\n> when this is detected.\n\nHm. So the fsmonitor listener for Linux is not reliable? Wouldn't the\nend user see the same issue then? I'm not sure whether just ignoring\nthat issue and adding a timeout to our tests is the proper way to fix\nit.\n\nBefore we jump to such solutions I'd rather want to know what the root\ncause of this. We had similar issues in the past on macOS, where we\neventually figured out that we were missing events due to the buffers\nnot being big enough. So did you investigate what the conditions are to\ntrigger this?\n\nPatrick\n"},{"id":"537763","messageId":"aafilb7FnvWBTQ0J@pks.im","threadId":"64696","inReplyTo":"4987a009a2e74a04aa1a8deb0508c971a3df0549.1772065643.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 10/10] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T07:43:17Z","receivedAt":"2026-03-04T07:43:22Z","isPatch":true,"body":"On Thu, Feb 26, 2026 at 12:27:23AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> When the fsmonitor daemon is spawned as a background process, it may\n> inherit file descriptors from its parent that it does not need.  In\n> particular, when the test harness or a CI system captures output through\n> pipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\n> holds these open, the parent process never sees EOF and may appear to\n> hang.\n\nOh, interesting.\n\n> Set close_fd_above_stderr on the child process at daemon startup so\n> that file descriptors 3 and above are closed before any daemon work\n> begins.  This ensures the daemon does not inadvertently hold open\n> descriptors from its launching environment.\n> \n> Additionally, call setsid() when the daemon starts with --detach to\n> create a new session and process group.  Without this, shells that\n> enable job control (e.g. bash with \"set -m\") treat the daemon as part\n> of the spawning command's job.  Their \"wait\" builtin then blocks until\n> the daemon exits, which it never does.  This specifically affects\n> systems where /bin/sh is bash (e.g. Fedora), since dash only waits for\n> the specific PID rather than the full process group.\n\nHm. We already have related logic in `daemonize()`. Should we maybe\nreuse that function, and potentially expand it to handle closing all FDs\nup to the maximum file descriptor?\n\n> Add a 30-second timeout to \"fsmonitor--daemon stop\" so it does\n> not block indefinitely if the daemon fails to shut down.\n\nThis feels like a \"while-at-it\" change to me. Should it maybe be moved\ninto a separate commit?\n\n> diff --git a/t/meson.build b/t/meson.build\n> index 85ef2ae2fa..19e8306298 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -1210,18 +1210,12 @@ test_environment = script_environment\n>  test_environment.set('GIT_BUILD_DIR', git_build_dir)\n>  \n>  foreach integration_test : integration_tests\n> -  per_test_kwargs = test_kwargs\n> -  # The fsmonitor tests start daemon processes that in some environments\n> -  # can hang.  Set a generous timeout to prevent CI from blocking.\n> -  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n> -    per_test_kwargs += {'timeout': 1800}\n> -  endif\n>    test(fs.stem(integration_test), shell,\n>      args: [ integration_test ],\n>      workdir: meson.current_source_dir(),\n>      env: test_environment,\n>      depends: test_dependencies + bin_wrappers,\n> -    kwargs: per_test_kwargs,\n> +    kwargs: test_kwargs,\n>    )\n>  endforeach\n>  \n\nMight make sense to reorder commits a bit so that the fix comes first.\nIn that case we wouldn't ever have to introduce the timeouts in the\nfirst place.\n\n> diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\n> index 774da5ac60..d7e64bcb7a 100755\n> --- a/t/t7527-builtin-fsmonitor.sh\n> +++ b/t/t7527-builtin-fsmonitor.sh\n> @@ -766,7 +766,7 @@ do\n>  \t\telse\n>  \t\t\ttest_expect_success \"Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor\" '\n>  \t\t\t\tgit config core.fsmonitor true &&\n> -\t\t\t\tgit fsmonitor--daemon start &&\n> +\t\t\t\tgit fsmonitor--daemon start --start-timeout=10 &&\n>  \t\t\t\tgit update-index --fsmonitor\n>  \t\t\t'\n>  \t\tfi\n\nThis change feels unrelated and is not mentioned in the commit message.\n\n> @@ -997,7 +997,17 @@ start_git_in_background () {\n>  \t\tnr_tries_left=$(($nr_tries_left - 1))\n>  \tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n>  \twatchdog_pid=$!\n> +\n> +\t# Disable job control before wait.  With \"set -m\", bash treats\n> +\t# \"wait $pid\" as waiting for the entire job (process group),\n> +\t# which blocks indefinitely if the fsmonitor daemon was spawned\n> +\t# into the same process group and is still running.  Turning off\n> +\t# job control makes \"wait\" only wait for the specific PID.\n> +\tset +m &&\n>  \twait $git_pid\n> +\twait_status=$?\n> +\tset -m\n> +\treturn $wait_status\n>  }\n\nI thought with our call to setsid() we're not part of the same process\ngroup anymore. So why is this change here still needed?\n\nPatrick\n"},{"id":"537800","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 01/12] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:14Z","receivedAt":"2026-03-04T18:15:30Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"537803","messageId":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v7.git.git.1772065643.gitgitgadget@gmail.com","subject":"[PATCH v8 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:13Z","receivedAt":"2026-03-04T18:15:30Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (12):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  11 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n 18 files changed, 1261 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v8\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v8\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v7:\n\n  1:  4d4dec8fa1 =  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  2:  d0bd3e32ca !  2:  cb270120f0 fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n     @@ Commit message\n      \n          The `state.cookies` hashmap is initialized during daemon startup but\n          never freed during cleanup in the `done:` label of\n     -    fsmonitor_run_daemon().  Add a hashmap_clear() call to prevent this\n     -    memory leak.\n     +    fsmonitor_run_daemon().  The cookie entries also have names allocated\n     +    via strbuf_detach() that must be freed individually.\n     +\n     +    Iterate the hashmap to free each cookie name, then call\n     +    hashmap_clear_and_free() to release the entries and table.\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n     @@ builtin/fsmonitor--daemon.c: static int fsmonitor_run_daemon(void)\n       done:\n       \tpthread_cond_destroy(&state.cookies_cond);\n       \tpthread_mutex_destroy(&state.main_lock);\n     -+\thashmap_clear(&state.cookies);\n     ++\t{\n     ++\t\tstruct hashmap_iter iter;\n     ++\t\tstruct fsmonitor_cookie_item *cookie;\n     ++\n     ++\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n     ++\t\t\tfree(cookie->name);\n     ++\t\thashmap_clear_and_free(&state.cookies,\n     ++\t\t\t\t       struct fsmonitor_cookie_item, entry);\n     ++\t}\n       \tfsm_listen__dtor(&state);\n       \tfsm_health__dtor(&state);\n       \n  3:  d2c5ca0939 !  3:  44a063074d compat/win32: add pthread_cond_timedwait\n     @@ Commit message\n          compatibility layer using SleepConditionVariableCS() with a millisecond\n          timeout computed from the absolute deadline.\n      \n     -    This enables callers to use bounded waits on condition variables\n     -    instead of blocking indefinitely with pthread_cond_wait().\n     -\n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## compat/win32/pthread.c ##\n     @@ compat/win32/pthread.c: int pthread_cond_wait(pthread_cond_t *cond, pthread_mute\n      +\t\t      abstime->tv_nsec / 1000000;\n      +\n      +\tif (deadline_ms <= now_ms)\n     -+\t\ttimeout_ms = 0;\n     ++\t\treturn ETIMEDOUT;\n      +\telse\n      +\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n      +\n  4:  0a58670952 =  4:  b1081d1e13 fsmonitor: use pthread_cond_timedwait for cookie wait\n  6:  0a83bb9c8e !  5:  dec0fb144f fsmonitor: deduplicate settings logic for Unix platforms\n     @@ Metadata\n      Author: Paul Tarjan <github@paulisageek.com>\n      \n       ## Commit message ##\n     -    fsmonitor: deduplicate settings logic for Unix platforms\n     +    fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n      \n     -    The macOS fsm-settings-darwin.c is applicable to other Unix variants\n     -    as well.  Rename it to fsm-settings-unix.c, using the safer\n     -    xstrdup()+dirname() approach and including the \"vfat\" filesystem check.\n     +    The fsmonitor IPC path logic in fsm-ipc-darwin.c is not\n     +    Darwin-specific and will be reused by the upcoming Linux\n     +    implementation.  Rename it to fsm-ipc-unix.c to reflect that it\n     +    is shared by all Unix platforms.\n      \n     -    Now that both fsm-ipc and fsm-settings use the \"unix\" variant name,\n     -    set FSMONITOR_OS_SETTINGS to \"unix\" for macOS in config.mak.uname and\n     -    remove the if/else conditionals from the build files.\n     +    Introduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\n     +    for Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\n     +    that the build files can distinguish between platform-specific files\n     +    (listen, health, path-utils) and shared Unix files (ipc, settings).\n      \n     +    Move fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\n     +    switch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\n     +    is platform-specific (there will be separate darwin and linux versions).\n     +\n     +    Based-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\n     +    Based-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## Makefile ##\n     +@@ Makefile: include shared.mak\n     + # If your platform has OS-specific ways to tell if a repo is incompatible with\n     + # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n     + # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n     +-# that implements the `fsm_os_settings__*()` routines.\n     ++# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n     + #\n     + # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n     + # programs in oss-fuzz/.\n      @@ Makefile: ifdef FSMONITOR_DAEMON_BACKEND\n       \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n       \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n       \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n     --ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n     --\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n     --else\n     --\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n     --endif\n     +-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n       endif\n       \n       ifdef FSMONITOR_OS_SETTINGS\n       \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n      +\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n       \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n     - \tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n     +-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n     ++\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n       endif\n     -\n     - ## compat/fsmonitor/fsm-settings-darwin.c => compat/fsmonitor/fsm-settings-unix.c ##\n     -@@\n     - #include \"fsmonitor-settings.h\"\n     - #include \"fsmonitor-path-utils.h\"\n     - \n     -- /*\n     -+/*\n     -  * For the builtin FSMonitor, we create the Unix domain socket for the\n     -  * IPC in the .git directory.  If the working directory is remote,\n     -  * then the socket will be created on the remote file system.  This\n     -@@\n     -  * The builtin FSMonitor uses a Unix domain socket in the .git\n     -  * directory for IPC.  These Windows drive formats do not support\n     -  * Unix domain sockets, so mark them as incompatible for the daemon.\n     -- *\n     -  */\n     - static enum fsmonitor_reason check_uds_volume(struct repository *r)\n     - {\n     - \tstruct fs_info fs;\n     - \tconst char *ipc_path = fsmonitor_ipc__get_path(r);\n     --\tstruct strbuf path = STRBUF_INIT;\n     --\tstrbuf_add(&path, ipc_path, strlen(ipc_path));\n     -+\tchar *path;\n     -+\tchar *dir;\n     -+\n     -+\t/*\n     -+\t * Create a copy for dirname() since it may modify its argument.\n     -+\t */\n     -+\tpath = xstrdup(ipc_path);\n     -+\tdir = dirname(path);\n       \n     --\tif (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {\n     --\t\tstrbuf_release(&path);\n     -+\tif (fsmonitor__get_fs_info(dir, &fs) == -1) {\n     -+\t\tfree(path);\n     - \t\treturn FSMONITOR_REASON_ERROR;\n     - \t}\n     - \n     --\tstrbuf_release(&path);\n     -+\tfree(path);\n     - \n     - \tif (fs.is_remote ||\n     --\t\t!strcmp(fs.typename, \"msdos\") ||\n     --\t\t!strcmp(fs.typename, \"ntfs\")) {\n     -+\t    !strcmp(fs.typename, \"msdos\") ||\n     -+\t    !strcmp(fs.typename, \"ntfs\") ||\n     -+\t    !strcmp(fs.typename, \"vfat\")) {\n     - \t\tfree(fs.typename);\n     - \t\treturn FSMONITOR_REASON_NOSOCKETS;\n     - \t}\n     + ifdef WITH_BREAKING_CHANGES\n     +\n     + ## compat/fsmonitor/fsm-ipc-darwin.c => compat/fsmonitor/fsm-ipc-unix.c ##\n      \n       ## config.mak.uname ##\n      @@ config.mak.uname: ifeq ($(uname_S),Darwin)\n     @@ contrib/buildsystems/CMakeLists.txt: endif()\n       \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n      -\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n      -\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n     --\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n      -\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n      +\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n      +\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n      +\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n       \n       \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n      -\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n       \tendif()\n       endif()\n       \n     @@ meson.build: else\n       endif\n       if fsmonitor_backend != ''\n      @@ meson.build: if fsmonitor_backend != ''\n     + \n     +   libgit_sources += [\n           'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n     +-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n     ++    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n           'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n           'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     --    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n     -+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     -+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n     +     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n         ]\n     --\n     --  if fsmonitor_backend == 'win32'\n     --    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n     --  else\n     --    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n     --  endif\n       endif\n       build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n      -build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n  5:  037ae2a03f !  6:  b2aaadb4ae fsmonitor: deduplicate IPC path logic for Unix platforms\n     @@ Metadata\n      Author: Paul Tarjan <github@paulisageek.com>\n      \n       ## Commit message ##\n     -    fsmonitor: deduplicate IPC path logic for Unix platforms\n     +    fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n      \n     -    The macOS fsm-ipc-darwin.c is applicable to other Unix variants as\n     -    well.  Rename it to fsm-ipc-unix.c and add a worktree NULL check\n     -    (BUG guard) that was missing from the macOS version.\n     +    The fsmonitor settings logic in fsm-settings-darwin.c is not\n     +    Darwin-specific and will be reused by the upcoming Linux\n     +    implementation.  Rename it to fsm-settings-unix.c to reflect that it\n     +    is shared by all Unix platforms.\n      \n     -    To support this, introduce FSMONITOR_OS_SETTINGS which is set to\n     -    \"unix\" for both macOS and Linux, distinct from FSMONITOR_DAEMON_BACKEND\n     -    which remains platform-specific (darwin, linux, win32).  Move\n     -    fsm-path-utils from FSMONITOR_OS_SETTINGS to FSMONITOR_DAEMON_BACKEND\n     -    since the path-utils files are platform-specific.\n     +    Update the build files (meson.build and CMakeLists.txt) to use\n     +    FSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\n     +    used for fsm-ipc.\n      \n     +    Based-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\n     +    Based-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n     - ## Makefile ##\n     -@@ Makefile: ifdef FSMONITOR_DAEMON_BACKEND\n     - \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n     - \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n     - \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n     --\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n     -+ifeq ($(FSMONITOR_DAEMON_BACKEND),win32)\n     -+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-win32.o\n     -+else\n     -+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o\n     -+endif\n     - endif\n     - \n     - ifdef FSMONITOR_OS_SETTINGS\n     - \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n     - \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n     --\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n     -+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n     - endif\n     - \n     - ifdef WITH_BREAKING_CHANGES\n     -\n     - ## compat/fsmonitor/fsm-ipc-darwin.c => compat/fsmonitor/fsm-ipc-unix.c ##\n     -@@ compat/fsmonitor/fsm-ipc-unix.c: const char *fsmonitor_ipc__get_path(struct repository *r)\n     - \tif (ipc_path)\n     - \t\treturn ipc_path;\n     - \n     --\n     - \t/* By default the socket file is created in the .git directory */\n     - \tif (fsmonitor__is_fs_remote(r->gitdir) < 1) {\n     - \t\tipc_path = fsmonitor_ipc__get_default_path();\n     - \t\treturn ipc_path;\n     - \t}\n     - \n     -+\tif (!r->worktree)\n     -+\t\tBUG(\"repository has no worktree\");\n     -+\n     - \tgit_SHA1_Init(&sha1ctx);\n     - \tgit_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));\n     - \tgit_SHA1_Final(hash, &sha1ctx);\n     + ## compat/fsmonitor/fsm-settings-darwin.c => compat/fsmonitor/fsm-settings-unix.c ##\n      \n       ## contrib/buildsystems/CMakeLists.txt ##\n      @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n     - \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n     --\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n     -+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)\n     - \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n     + \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n       \n       \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n     +-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n     ++\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n     + \tendif()\n     + endif()\n     + \n      \n       ## meson.build ##\n      @@ meson.build: if fsmonitor_backend != ''\n     - \n     -   libgit_sources += [\n     -     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n     --    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n     +     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n           'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n           'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     -     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n     +-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n     ++    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n         ]\n     -+\n     -+  if fsmonitor_backend == 'win32'\n     -+    libgit_sources += 'compat/fsmonitor/fsm-ipc-win32.c'\n     -+  else\n     -+    libgit_sources += 'compat/fsmonitor/fsm-ipc-unix.c'\n     -+  endif\n       endif\n       build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n     - build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n  7:  adac7964cc !  7:  03cf12d01b fsmonitor: implement filesystem change listener for Linux\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\t\t\tif (errno == EAGAIN || errno == EINTR)\n      +\t\t\t\tgoto done;\n      +\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n     -+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n     ++\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n      +\t\t\tgoto done;\n      +\t\t}\n      +\n     @@ compat/fsmonitor/fsm-listen-linux.c (new)\n      +\t\t\t/* File system was unmounted or event queue overflowed */\n      +\t\t\tif (em_force_shutdown(event->mask)) {\n      +\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n     -+\t\t\t\t\tlog_mask_set(\"Forcing shutdown\", event->mask);\n     ++\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n      +\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n      +\t\t\t\tgoto done;\n      +\t\t\t}\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +#ifndef CODA_SUPER_MAGIC\n      +#define CODA_SUPER_MAGIC 0x73757245\n      +#endif\n     -+#ifndef V9FS_MAGIC\n     -+#define V9FS_MAGIC 0x01021997\n     -+#endif\n      +#ifndef FUSE_SUPER_MAGIC\n      +#define FUSE_SUPER_MAGIC 0x65735546\n      +#endif\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +}\n      +\n      +/*\n     -+ * Get the filesystem type name for logging purposes.\n     ++ * Map filesystem magic numbers to human-readable names as a fallback\n     ++ * when /proc/mounts is unavailable.  This only covers the remote and\n     ++ * special filesystems in is_remote_fs() above; local filesystems are\n     ++ * never flagged as incompatible, so we do not need their names here.\n      + */\n      +static const char *get_fs_typename(unsigned long f_type)\n      +{\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +\t\treturn \"afs\";\n      +\tcase CODA_SUPER_MAGIC:\n      +\t\treturn \"coda\";\n     -+\tcase V9FS_MAGIC:\n     -+\t\treturn \"9p\";\n      +\tcase FUSE_SUPER_MAGIC:\n      +\t\treturn \"fuse\";\n      +\tdefault:\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +\n      +/*\n      + * Find the mount point for a given path by reading /proc/mounts.\n     -+ * Returns the filesystem type for the longest matching mount point.\n     ++ *\n     ++ * statfs(2) gives us f_type (the magic number) but not the human-readable\n     ++ * filesystem type string.  We scan /proc/mounts to find the mount entry\n     ++ * whose path is the longest prefix of ours and whose f_fsid matches,\n     ++ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n      + */\n     -+static char *find_mount(const char *path, struct statfs *fs)\n     ++static char *find_mount(const char *path, const struct statfs *path_fs)\n      +{\n      +\tFILE *fp;\n      +\tstruct strbuf line = STRBUF_INIT;\n      +\tstruct strbuf match = STRBUF_INIT;\n      +\tstruct strbuf fstype = STRBUF_INIT;\n      +\tchar *result = NULL;\n     -+\tstruct statfs path_fs;\n     -+\n     -+\tif (statfs(path, &path_fs) < 0)\n     -+\t\treturn NULL;\n      +\n      +\tfp = fopen(\"/proc/mounts\", \"r\");\n      +\tif (!fp)\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n      +\t\t\t\t/* Check if filesystem ID matches */\n      +\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n     -+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,\n     ++\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n      +\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n      +\t\t\t\t\t/* Keep the longest matching mount point */\n      +\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)\n      +\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n      +\t\t\t\t\t\tstrbuf_reset(&fstype);\n      +\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n     -+\t\t\t\t\t\t*fs = mount_fs;\n      +\t\t\t\t\t}\n      +\t\t\t\t}\n      +\t\t\t}\n     @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)\n       \tendif()\n       \n       \tif(FSMONITOR_DAEMON_BACKEND)\n     +@@ contrib/buildsystems/CMakeLists.txt: endif()\n     + file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n     + string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n     + string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n     +-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n     +-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n     ++string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n     ++string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n     + string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n     + string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n     + string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\n      \n       ## meson.build ##\n      @@ meson.build: fsmonitor_os = ''\n  9:  c684fc9094 !  8:  29a6461915 run-command: add close_fd_above_stderr option\n     @@ Commit message\n          run-command: add close_fd_above_stderr option\n      \n          Add a new option to struct child_process that closes file descriptors\n     -    3 and above in the child after forking but before exec.  This prevents\n     -    long-running child processes from inheriting pipe endpoints or other\n     +    3 and above in the child after forking but before exec.  Without this,\n     +    long-running child processes inherit pipe endpoints and other\n          descriptors from the parent environment.\n      \n          The upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\n 10:  4987a009a2 !  9:  b596c5004d fsmonitor: close inherited file descriptors and detach in daemon\n     @@ Commit message\n          holds these open, the parent process never sees EOF and may appear to\n          hang.\n      \n     -    Set close_fd_above_stderr on the child process at daemon startup so\n     -    that file descriptors 3 and above are closed before any daemon work\n     -    begins.  This ensures the daemon does not inadvertently hold open\n     -    descriptors from its launching environment.\n     +    Set close_fd_above_stderr on the child process at both daemon startup\n     +    paths: the explicit \"fsmonitor--daemon start\" command and the implicit\n     +    spawn triggered by fsmonitor-ipc when a client finds no running daemon.\n     +    Also suppress stdout and stderr on the implicit spawn path to prevent\n     +    the background daemon from writing to the client's terminal.\n      \n          Additionally, call setsid() when the daemon starts with --detach to\n     -    create a new session and process group.  Without this, shells that\n     -    enable job control (e.g. bash with \"set -m\") treat the daemon as part\n     -    of the spawning command's job.  Their \"wait\" builtin then blocks until\n     -    the daemon exits, which it never does.  This specifically affects\n     -    systems where /bin/sh is bash (e.g. Fedora), since dash only waits for\n     -    the specific PID rather than the full process group.\n     -\n     -    Add a 30-second timeout to \"fsmonitor--daemon stop\" so it does\n     -    not block indefinitely if the daemon fails to shut down.\n     +    create a new session and process group.  This prevents the daemon\n     +    from being part of the spawning shell's process group, which could\n     +    cause the shell's \"wait\" to block until the daemon exits.\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## builtin/fsmonitor--daemon.c ##\n     -@@ builtin/fsmonitor--daemon.c: static int do_as_client__send_stop(void)\n     - {\n     - \tstruct strbuf answer = STRBUF_INIT;\n     - \tint ret;\n     -+\tint max_wait_ms = 30000;\n     -+\tint elapsed_ms = 0;\n     - \n     - \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n     - \n     -@@ builtin/fsmonitor--daemon.c: static int do_as_client__send_stop(void)\n     - \t\treturn ret;\n     - \n     - \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n     --\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n     -+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n     -+\t\tif (elapsed_ms >= max_wait_ms) {\n     -+\t\t\ttrace2_region_leave(\"fsm_client\",\n     -+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n     -+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n     -+\t\t\t\t     max_wait_ms / 1000);\n     -+\t\t}\n     - \t\tsleep_millisec(50);\n     -+\t\telapsed_ms += 50;\n     -+\t}\n     - \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n     - \n     - \treturn 0;\n      @@ builtin/fsmonitor--daemon.c: done:\n       \treturn err;\n       }\n     @@ fsmonitor-ipc.c: static int spawn_daemon(void)\n       \tcmd.trace2_child_class = \"fsmonitor\";\n       \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n       \n     -\n     - ## t/meson.build ##\n     -@@ t/meson.build: test_environment = script_environment\n     - test_environment.set('GIT_BUILD_DIR', git_build_dir)\n     - \n     - foreach integration_test : integration_tests\n     --  per_test_kwargs = test_kwargs\n     --  # The fsmonitor tests start daemon processes that in some environments\n     --  # can hang.  Set a generous timeout to prevent CI from blocking.\n     --  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n     --    per_test_kwargs += {'timeout': 1800}\n     --  endif\n     -   test(fs.stem(integration_test), shell,\n     -     args: [ integration_test ],\n     -     workdir: meson.current_source_dir(),\n     -     env: test_environment,\n     -     depends: test_dependencies + bin_wrappers,\n     --    kwargs: per_test_kwargs,\n     -+    kwargs: test_kwargs,\n     -   )\n     - endforeach\n     - \n     -\n     - ## t/t7527-builtin-fsmonitor.sh ##\n     -@@ t/t7527-builtin-fsmonitor.sh: do\n     - \t\telse\n     - \t\t\ttest_expect_success \"Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor\" '\n     - \t\t\t\tgit config core.fsmonitor true &&\n     --\t\t\t\tgit fsmonitor--daemon start &&\n     -+\t\t\t\tgit fsmonitor--daemon start --start-timeout=10 &&\n     - \t\t\t\tgit update-index --fsmonitor\n     - \t\t\t'\n     - \t\tfi\n     -@@ t/t7527-builtin-fsmonitor.sh: start_git_in_background () {\n     - \t\tnr_tries_left=$(($nr_tries_left - 1))\n     - \tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n     - \twatchdog_pid=$!\n     -+\n     -+\t# Disable job control before wait.  With \"set -m\", bash treats\n     -+\t# \"wait $pid\" as waiting for the entire job (process group),\n     -+\t# which blocks indefinitely if the fsmonitor daemon was spawned\n     -+\t# into the same process group and is still running.  Turning off\n     -+\t# job control makes \"wait\" only wait for the specific PID.\n     -+\tset +m &&\n     - \twait $git_pid\n     -+\twait_status=$?\n     -+\tset -m\n     -+\treturn $wait_status\n     - }\n     - \n     - stop_git () {\n  -:  ---------- > 10:  72125ac20f fsmonitor: add timeout to daemon stop command\n  8:  fad2f0a81a ! 11:  76b171ab5c fsmonitor: add tests for Linux\n     @@ Commit message\n          as harmless.  An earlier version of the listener crashed in this\n          scenario.\n      \n     -    Signed-off-by: Paul Tarjan <github@paulisageek.com>\n     +    Reduce --start-timeout from the default 60 seconds to 10 seconds so\n     +    that tests fail promptly when the daemon cannot start.\n      \n     - ## t/meson.build ##\n     -@@ t/meson.build: test_environment = script_environment\n     - test_environment.set('GIT_BUILD_DIR', git_build_dir)\n     - \n     - foreach integration_test : integration_tests\n     -+  per_test_kwargs = test_kwargs\n     -+  # The fsmonitor tests start daemon processes that in some environments\n     -+  # can hang.  Set a generous timeout to prevent CI from blocking.\n     -+  if fs.stem(integration_test) == 't7527-builtin-fsmonitor'\n     -+    per_test_kwargs += {'timeout': 1800}\n     -+  endif\n     -   test(fs.stem(integration_test), shell,\n     -     args: [ integration_test ],\n     -     workdir: meson.current_source_dir(),\n     -     env: test_environment,\n     -     depends: test_dependencies + bin_wrappers,\n     --    kwargs: test_kwargs,\n     -+    kwargs: per_test_kwargs,\n     -   )\n     - endforeach\n     - \n     +    Harden the test helpers to work in environments without procps\n     +    (e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\n     +    process group ID when ps is unavailable, guard stop_git() against\n     +    an empty pgid, and redirect stderr from kill to /dev/null to avoid\n     +    noise when processes have already exited.\n     +\n     +    Use set -m to enable job control in the submodule-pull test so that\n     +    the background git pull gets its own process group, preventing the\n     +    shell wait from blocking on the daemon.  setsid() in the previous\n     +    commit detaches the daemon itself, but the intermediate git pull\n     +    process still needs its own process group for the test shell to\n     +    manage it correctly.\n     +\n     +    Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## t/t7527-builtin-fsmonitor.sh ##\n      @@ t/t7527-builtin-fsmonitor.sh: then\n  -:  ---------- > 12:  6c36c9e11e fsmonitor: convert shown khash to strset in do_handle_client\n\n-- \ngitgitgadget\n"},{"id":"537801","messageId":"cb270120f0e27a34a58c856b7c80e78e2301c989.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 02/12] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:15Z","receivedAt":"2026-03-04T18:15:32Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"537802","messageId":"44a063074dadc27110e2dd3481756c8c324ed956.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 03/12] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:16Z","receivedAt":"2026-03-04T18:15:33Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"537804","messageId":"b1081d1e1370bb9c9755d0635e7c7466c954f046.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 04/12] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:17Z","receivedAt":"2026-03-04T18:15:35Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"537805","messageId":"dec0fb144f9ea4efe8d15526b376f51ef470ed5d.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 05/12] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:18Z","receivedAt":"2026-03-04T18:15:36Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"537806","messageId":"b2aaadb4aea37729989c6cd08838d7aee5b9dc4c.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 06/12] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:19Z","receivedAt":"2026-03-04T18:15:37Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537808","messageId":"03cf12d01b8d75610b4c35ff412cfe6cd222a171.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 07/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:20Z","receivedAt":"2026-03-04T18:15:39Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"537807","messageId":"29a6461915ce9d2abedb29e475d589bb8d24934a.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 08/12] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:21Z","receivedAt":"2026-03-04T18:15:40Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a new option to struct child_process that closes file descriptors\n3 and above in the child after forking but before exec.  Without this,\nlong-running child processes inherit pipe endpoints and other\ndescriptors from the parent environment.\n\nThe upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\ncapped at 4096 to avoid excessive iteration when the limit is set\nvery high.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 11 +++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 20 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..cbadcf5ff8 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -832,6 +832,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..a1aa1b1069 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the long-running child from\n+\t * inheriting pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"537809","messageId":"b596c5004d8c4c706d890192c40c570afdcfc179.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 09/12] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:22Z","receivedAt":"2026-03-04T18:15:42Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..b2a816dc3f 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"537810","messageId":"72125ac20fd0e4d8fdef419feed405c6c45ca0f5.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 10/12] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:23Z","receivedAt":"2026-03-04T18:15:44Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex b2a816dc3f..53d8ad1f0d 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"537811","messageId":"76b171ab5c3d367e8f8fd825a7e296c399d26a95.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 11/12] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:24Z","receivedAt":"2026-03-04T18:15:45Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"537812","messageId":"6c36c9e11e8d3ab55bb1bb34e3cff8c2fc118011.1772648125.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v8 12/12] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T18:15:25Z","receivedAt":"2026-03-04T18:15:47Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 53d8ad1f0d..f920cf3a82 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"537813","messageId":"20260304181740.25613-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafiaS-DscdIkori@pks.im","subject":"Re: [PATCH v7 01/10] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:40Z","receivedAt":"2026-03-04T18:17:43Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> Makes sense. If I understood correctly I think we could improve this\n> code to stop using khash directly and instead use a strmap, which\n> has a nicer interface. But that's certainly outside of the scope of this\n> patch series and rather a #leftoverbit.\n\nWent ahead and did this in v8 as patch 12. Switched to strset\n(since we only need a set, not a map). Ended up being a nice\nsimplification: strset_add() returns whether the entry is new, so\nthe lookup+insert becomes a single call.\n"},{"id":"537814","messageId":"20260304181742.25634-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aaficy025yUqsgN5@pks.im","subject":"Re: [PATCH v7 02/10] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:42Z","receivedAt":"2026-03-04T18:17:44Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> Is this actually sufficient? as far as I can see, the cookies are\n> inserted in `__wait_for_cookie()`, and each cookie also has a name\n> attached to it that was allocated via a strbuf. So don't we have to\n> free the name, as well?\n\nYou're right, the cookie names come from strbuf_detach() so they need\nto be freed too. Fixed in v8: iterates and frees each name before\ncalling hashmap_clear_and_free().\n"},{"id":"537815","messageId":"20260304181744.25653-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafieD42pMaYsnRw@pks.im","subject":"Re: [PATCH v7 03/10] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:44Z","receivedAt":"2026-03-04T18:17:46Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> So I guess it's safe to return ETIMEDOUT directly here?\n\nYep, that matches the POSIX spec. Fixed in v8.\n\n> Wouldn't it make sense to extend `err_win_to_posix()` instead?\n\nerr_win_to_posix() currently maps ERROR_TIMEOUT to EBUSY, not\nETIMEDOUT. Changing that mapping could affect other callers, so it\nseemed safer to handle WAIT_TIMEOUT explicitly in this function.\n"},{"id":"537816","messageId":"20260304181745.25673-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafifU-befdZW4O0@pks.im","subject":"Re: [PATCH v7 04/10] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:45Z","receivedAt":"2026-03-04T18:17:48Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> One thing that I'd be happy to learn about is why specifically you have\n> chosen one second as a timeout value. Are we sure this is always enough\n> on a loaded system?\n\nThe cookie round-trip (create temp file, get inotify event) is normally\na few milliseconds on a local filesystem, so 1 second gives plenty of\nheadroom. If it does fire on a heavily loaded box, the fallback is a\ntrivial (full-scan) response, same as any other error path, so the\nuser still gets correct results, just with a bit more work on their end.\n\nGoing much longer means the client just sits there waiting when\nsomething is actually broken (like overlayfs not delivering events),\nwhich is worse. Open to bumping it if you think 1s is too aggressive\nthough.\n"},{"id":"537817","messageId":"20260304181747.25692-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafigc1QiysIByhM@pks.im","subject":"Re: [PATCH v7 05/10] fsmonitor: deduplicate IPC path logic for Unix platforms","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:47Z","receivedAt":"2026-03-04T18:17:49Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> Nit: we're not exactly deduplicating just yet, but are rather preparing\n> for that as there is no second implementation using this yet.\n\nFair, renamed the commit to \"fsmonitor: rename fsm-ipc-darwin.c to\nfsm-ipc-unix.c\".\n\n> I think these while-at-it changes should be removed from this commit.\n\nDone, the rename is now 100% content-identical (the BUG guard and\nblank line removal are gone).\n"},{"id":"537818","messageId":"20260304181749.25711-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafihm4MVoVOaD2l@pks.im","subject":"Re: [PATCH v7 06/10] fsmonitor: deduplicate settings logic for Unix platforms","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:49Z","receivedAt":"2026-03-04T18:17:51Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> It's a bit weird that the only change to the Makefile here is to change\n> how we wire up fsm-ipc even though it's fsm-settings that this commit\n> cares about. Should this change be moved into the preceding commit?\n\nMoved. The Makefile fsm-ipc change is now in the IPC rename commit\n(patch 5). The settings commit has no Makefile changes since\nfsm-settings already used FSMONITOR_OS_SETTINGS.\n\n> Same here, it's not clear to me where those while-at-it changes are\n> coming from and whether we need them here. I'd rather drop them.\n\nDropped. Both renames are now pure renames with no content changes.\n\n> I think it might make sense to introduce the `fsmonitor_os` variable in\n> the preceding commit already.\n\nDone, fsmonitor_os is introduced in the IPC rename commit now.\n\n> We could avoid the flip-flopping of the code here.\n\nYeah, with fsmonitor_os in the IPC commit, the settings commit just\nflips fsm-settings from fsmonitor_backend to fsmonitor_os. No more\nflip-flopping.\n"},{"id":"537819","messageId":"20260304181750.25730-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafii97SoHS96rkA@pks.im","subject":"Re: [PATCH v7 07/10] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:50Z","receivedAt":"2026-03-04T18:17:52Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> I saw that this define is only wired up for CMake. I guess we should\n> also add it to our Makefile (probably via config.mak.uname) and Meson\n> (probably via compiler.has_header()).\n\nIt's already wired up in all three:\n\n  - config.mak.uname line 78: BASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n  - meson.build: compiler.has_header('linux/magic.h') check\n  - CMakeLists.txt: add_compile_definitions(HAVE_LINUX_MAGIC_H)\n\nWhile looking at CMakeLists.txt I noticed the GIT-BUILD-OPTIONS\nstring replacements were hardcoded to \"win32\" for both\nFSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS. Fixed that to\nuse the CMake variables so Linux/macOS builds get the right values.\n\n> This selection looks rather interesting to me. Why wouldn't we include\n> common filesystems like ext4 and the like? Certainly hints that the\n> function needs better documentation, and potentially a better name.\n\nIt's only used as a fallback when /proc/mounts isn't available. Since\nwe only care about naming the remote/special filesystems that\nis_remote_fs() flags as incompatible, there's no need for ext4 etc.\nUpdated the comment in v8 to make this clearer. Also removed\nV9FS_MAGIC from this function since 9p is used for local VM/container\nhost mounts where fsmonitor works fine, and it's not in is_remote_fs().\n\n> Sorry, but I still don't quite understand what we're doing here. Isn't\n> the longest matching mount point always the one that statfs(3p) gave us?\n> Why do we have to scan \"/proc/mounts\"?\n\nstatfs(2) gives us f_type (magic number) and f_fsid but not the\nhuman-readable filesystem type string. We need the type name (e.g.\n\"nfs\", \"cifs\") for the fs_info.typename field, which check_uds_volume()\ncompares against strings like \"msdos\" and \"ntfs\". /proc/mounts has that\nstring, and we match on f_fsid to find the right entry.\n\nAlso fixed a redundant statfs() call: find_mount() was calling\nstatfs(path, ...) again even though the caller already had the result.\nChanged it to take a const pointer to the caller's statfs instead.\n"},{"id":"537820","messageId":"20260304181752.25768-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafikA4bQS3lB0Hq@pks.im","subject":"Re: [PATCH v7 08/10] fsmonitor: add tests for Linux","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:52Z","receivedAt":"2026-03-04T18:17:54Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> Hm. So the fsmonitor listener for Linux is not reliable? Wouldn't the\n> end user see the same issue then? I'm not sure whether just ignoring\n> that issue and adding a timeout to our tests is the proper way to fix\n> it.\n>\n> Before we jump to such solutions I'd rather want to know what the root\n> cause of this. We had similar issues in the past on macOS, where we\n> eventually figured out that we were missing events due to the buffers\n> not being big enough. So did you investigate what the conditions are to\n> trigger this?\n\nThis isn't a buffer issue or a bug in our code, it's a kernel\nlimitation in overlayfs. On older kernels, overlayfs doesn't implement\nthe fsnotify hooks that inotify depends on, so inotify_add_watch\nsucceeds but events are silently never delivered. Newer kernels have\npartially fixed this but it still affects some configurations.\n\nAn end user on overlayfs would hit the same thing: the cookie wait\ntimes out and they get a full-scan fallback. Correct results, just\nno incremental speedup. The smoke test catches this early so we can\nskip instead of timing out on every subsequent test.\n"},{"id":"537821","messageId":"20260304181753.25787-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aafilb7FnvWBTQ0J@pks.im","subject":"Re: [PATCH v7 10/10] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-04T18:17:53Z","receivedAt":"2026-03-04T18:17:56Z","isPatch":true,"body":"On Tue, Mar 4, 2026, Patrick Steinhardt wrote:\n> Hm. We already have related logic in `daemonize()`. Should we maybe\n> reuse that function, and potentially expand it to handle closing all FDs\n> up to the maximum file descriptor?\n\ndaemonize() does a double-fork with setsid, which is the classic Unix\ndaemon pattern. But fsmonitor uses start_bg_command(), which forks the\ndaemon and waits for it to signal readiness over IPC. If we called\ndaemonize() inside the child, start_bg_command() would lose track of\nthe PID.\n\nSo instead we just use setsid() to detach from the terminal, and\nclose_fd_above_stderr in run-command to close inherited FDs before exec.\n\n> This feels like a \"while-at-it\" change to me. Should it maybe be moved\n> into a separate commit?\n\nDone. Split the 30-second stop timeout into its own commit (patch 10,\n\"fsmonitor: add timeout to daemon stop command\").\n\n> Might make sense to reorder commits a bit so that the fix comes first.\n> In that case we wouldn't ever have to introduce the timeouts in the\n> first place.\n\nDone. Reordered in v8 so run-command and daemon detach come before\nthe test commit. The meson timeout never appears now.\n\n> This change feels unrelated and is not mentioned in the commit message.\n\n--start-timeout=10 is now in the tests commit (patch 11) and documented\nin that commit message.\n\n> I thought with our call to setsid() we're not part of the same process\n> group anymore. So why is this change here still needed?\n\nsetsid() runs inside the daemon after it's already been forked. The\nset -m in the test is about the shell putting `git pull &` into its own\nprocess group. Without it, the background job inherits the test\nshell's pgid and `wait` stalls. Moved to the tests commit (patch 11)\nwith a note in the commit message explaining why it's still needed.\n"},{"id":"537837","messageId":"xmqq8qc771zf.fsf@gitster.g","threadId":"64696","inReplyTo":"29a6461915ce9d2abedb29e475d589bb8d24934a.1772648125.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v8 08/12] run-command: add close_fd_above_stderr option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-04T20:51:00Z","receivedAt":"2026-03-04T20:51:03Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> Add a new option to struct child_process that closes file descriptors\n> 3 and above in the child after forking but before exec.  Without this,\n> long-running child processes inherit pipe endpoints and other\n> descriptors from the parent environment.\n>\n> The upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\n> capped at 4096 to avoid excessive iteration when the limit is set\n> very high.\n>\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  run-command.c | 11 +++++++++++\n>  run-command.h |  9 +++++++++\n>  2 files changed, 20 insertions(+)\n>\n> diff --git a/run-command.c b/run-command.c\n> index e3e02475cc..cbadcf5ff8 100644\n> --- a/run-command.c\n> +++ b/run-command.c\n> @@ -832,6 +832,17 @@ fail_pipe:\n>  \t\t\tchild_close(cmd->out);\n>  \t\t}\n>  \n> +\t\tif (cmd->close_fd_above_stderr) {\n> +\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n> +\t\t\tint fd;\n> +\t\t\tif (max_fd < 0 || max_fd > 4096)\n> +\t\t\t\tmax_fd = 4096;\n> +\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n> +\t\t\t\tif (fd != child_notifier)\n> +\t\t\t\t\tclose(fd);\n> +\t\t\t}\n> +\t\t}\n> +\n>  \t\tif (cmd->dir && chdir(cmd->dir))\n>  \t\t\tchild_die(CHILD_ERR_CHDIR);\n\nThe need for this particular \"close file descriptors other than the\nstandard ones\" may be common enough that I do not mind to have it\ninside \"run-command.c\", but I wonder if a generic callback function\nto call here in the child between fork and exec that the caller can\nsupply would be a good thing to have.  Then, any caller who may want\nto set close_fd_above_stderr could instead prepare a callback that\ndoes the body of the above if statement themselves.\n\n> diff --git a/run-command.h b/run-command.h\n> index 0df25e445f..a1aa1b1069 100644\n> --- a/run-command.h\n> +++ b/run-command.h\n> @@ -141,6 +141,15 @@ struct child_process {\n>  \tunsigned stdout_to_stderr:1;\n>  \tunsigned clean_on_exit:1;\n>  \tunsigned wait_after_clean:1;\n> +\n> +\t/**\n> +\t * Close file descriptors 3 and above in the child after forking\n> +\t * but before exec.  This prevents the long-running child from\n> +\t * inheriting pipe endpoints or other descriptors from the parent\n> +\t * environment (e.g., the test harness).\n> +\t */\n> +\tunsigned close_fd_above_stderr:1;\n> +\n>  \tvoid (*clean_on_exit_handler)(struct child_process *process);\n>  };\n"},{"id":"537855","messageId":"20260305004959.83647-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqq8qc771zf.fsf@gitster.g","subject":"Re: [PATCH v8 09/12] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-05T00:49:59Z","receivedAt":"2026-03-05T00:50:03Z","isPatch":true,"body":"On Wed, Mar 4, 2026, Junio C Hamano wrote:\n> I wonder if a generic callback function\n> to call here in the child between fork and exec that the caller can\n> supply would be a good thing to have.\n\nDone in v9. Replaced the close_fd_above_stderr flag with a pre_exec_cb\nfunction pointer on struct child_process. The fd-closing logic is now\na standalone close_fd_above_stderr() function that the two fsmonitor\ncallers pass as the callback.\n"},{"id":"537856","messageId":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v8.git.git.1772648125.gitgitgadget@gmail.com","subject":"[PATCH v9 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:48Z","receivedAt":"2026-03-05T00:52:04Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review\n * Provided close_fd_above_stderr() as a ready-made callback function\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (12):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add pre-exec callback for child processes\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  15 +\n run-command.h                                 |  15 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n 18 files changed, 1271 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v9\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v9\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v8:\n\n  1:  4d4dec8fa1 =  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  2:  cb270120f0 =  2:  cb270120f0 fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  3:  44a063074d =  3:  44a063074d compat/win32: add pthread_cond_timedwait\n  4:  b1081d1e13 =  4:  b1081d1e13 fsmonitor: use pthread_cond_timedwait for cookie wait\n  5:  dec0fb144f =  5:  dec0fb144f fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  6:  b2aaadb4ae =  6:  b2aaadb4ae fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  7:  03cf12d01b =  7:  03cf12d01b fsmonitor: implement filesystem change listener for Linux\n  8:  29a6461915 !  8:  31fa1fb324 run-command: add close_fd_above_stderr option\n     @@ Metadata\n      Author: Paul Tarjan <github@paulisageek.com>\n      \n       ## Commit message ##\n     -    run-command: add close_fd_above_stderr option\n     +    run-command: add pre-exec callback for child processes\n      \n     -    Add a new option to struct child_process that closes file descriptors\n     -    3 and above in the child after forking but before exec.  Without this,\n     -    long-running child processes inherit pipe endpoints and other\n     -    descriptors from the parent environment.\n     +    Add a pre_exec_cb function pointer to struct child_process that is\n     +    invoked in the child between fork and exec.  This gives callers a\n     +    place to perform setup that must happen in the child's context,\n     +    such as closing inherited file descriptors.\n      \n     -    The upper bound for the fd scan comes from sysconf(_SC_OPEN_MAX),\n     -    capped at 4096 to avoid excessive iteration when the limit is set\n     -    very high.\n     +    Provide close_fd_above_stderr() as a ready-made callback that\n     +    closes file descriptors 3 and above (skipping the child-notifier\n     +    pipe), capped at sysconf(_SC_OPEN_MAX) or 4096, whichever is\n     +    smaller.\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## run-command.c ##\n     +@@ run-command.c: static void trace_run_command(const struct child_process *cp)\n     + \tstrbuf_release(&buf);\n     + }\n     + \n     ++void close_fd_above_stderr(void)\n     ++{\n     ++\tlong max_fd = sysconf(_SC_OPEN_MAX);\n     ++\tint fd;\n     ++\tif (max_fd < 0 || max_fd > 4096)\n     ++\t\tmax_fd = 4096;\n     ++\tfor (fd = 3; fd < max_fd; fd++) {\n     ++\t\tif (fd != child_notifier)\n     ++\t\t\tclose(fd);\n     ++\t}\n     ++}\n     ++\n     + int start_command(struct child_process *cmd)\n     + {\n     + \tint need_in, need_out, need_err;\n      @@ run-command.c: fail_pipe:\n       \t\t\tchild_close(cmd->out);\n       \t\t}\n       \n     -+\t\tif (cmd->close_fd_above_stderr) {\n     -+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n     -+\t\t\tint fd;\n     -+\t\t\tif (max_fd < 0 || max_fd > 4096)\n     -+\t\t\t\tmax_fd = 4096;\n     -+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n     -+\t\t\t\tif (fd != child_notifier)\n     -+\t\t\t\t\tclose(fd);\n     -+\t\t\t}\n     -+\t\t}\n     ++\t\tif (cmd->pre_exec_cb)\n     ++\t\t\tcmd->pre_exec_cb();\n      +\n       \t\tif (cmd->dir && chdir(cmd->dir))\n       \t\t\tchild_die(CHILD_ERR_CHDIR);\n     @@ run-command.h: struct child_process {\n       \tunsigned wait_after_clean:1;\n      +\n      +\t/**\n     -+\t * Close file descriptors 3 and above in the child after forking\n     -+\t * but before exec.  This prevents the long-running child from\n     -+\t * inheriting pipe endpoints or other descriptors from the parent\n     -+\t * environment (e.g., the test harness).\n     ++\t * If set, the callback is invoked in the child between fork and\n     ++\t * exec.  It can be used, for example, to close inherited file\n     ++\t * descriptors that the child should not keep open.\n      +\t */\n     -+\tunsigned close_fd_above_stderr:1;\n     ++\tvoid (*pre_exec_cb)(void);\n      +\n       \tvoid (*clean_on_exit_handler)(struct child_process *process);\n       };\n       \n     +@@ run-command.h: struct child_process {\n     + \t.env = STRVEC_INIT, \\\n     + }\n     + \n     ++/**\n     ++ * Close file descriptors 3 and above.  Suitable for use as a\n     ++ * pre_exec_cb to prevent the child from inheriting pipe endpoints\n     ++ * or other descriptors from the parent environment.\n     ++ */\n     ++void close_fd_above_stderr(void);\n     ++\n     + /**\n     +  * The functions: start_command, finish_command, run_command do the following:\n     +  *\n  9:  b596c5004d !  9:  c963074cbd fsmonitor: close inherited file descriptors and detach in daemon\n     @@ builtin/fsmonitor--daemon.c: static int try_to_start_background_daemon(void)\n       \tcp.no_stdin = 1;\n       \tcp.no_stdout = 1;\n       \tcp.no_stderr = 1;\n     -+\tcp.close_fd_above_stderr = 1;\n     ++\tcp.pre_exec_cb = close_fd_above_stderr;\n       \n       \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n       \t\t\t\tfsmonitor__start_timeout_sec);\n     @@ fsmonitor-ipc.c: static int spawn_daemon(void)\n       \tcmd.no_stdin = 1;\n      +\tcmd.no_stdout = 1;\n      +\tcmd.no_stderr = 1;\n     -+\tcmd.close_fd_above_stderr = 1;\n     ++\tcmd.pre_exec_cb = close_fd_above_stderr;\n       \tcmd.trace2_child_class = \"fsmonitor\";\n       \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n       \n 10:  72125ac20f = 10:  ee3ee75c94 fsmonitor: add timeout to daemon stop command\n 11:  76b171ab5c = 11:  54bd8f604a fsmonitor: add tests for Linux\n 12:  6c36c9e11e = 12:  e603fc7dde fsmonitor: convert shown khash to strset in do_handle_client\n\n-- \ngitgitgadget\n"},{"id":"537857","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 01/12] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:49Z","receivedAt":"2026-03-05T00:52:05Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"537858","messageId":"cb270120f0e27a34a58c856b7c80e78e2301c989.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 02/12] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:50Z","receivedAt":"2026-03-05T00:52:07Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"537859","messageId":"44a063074dadc27110e2dd3481756c8c324ed956.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 03/12] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:51Z","receivedAt":"2026-03-05T00:52:08Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"537860","messageId":"b1081d1e1370bb9c9755d0635e7c7466c954f046.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 04/12] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:52Z","receivedAt":"2026-03-05T00:52:10Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"537861","messageId":"dec0fb144f9ea4efe8d15526b376f51ef470ed5d.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 05/12] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:53Z","receivedAt":"2026-03-05T00:52:11Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"537862","messageId":"b2aaadb4aea37729989c6cd08838d7aee5b9dc4c.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 06/12] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:54Z","receivedAt":"2026-03-05T00:52:13Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537864","messageId":"03cf12d01b8d75610b4c35ff412cfe6cd222a171.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 07/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:55Z","receivedAt":"2026-03-05T00:52:15Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"537863","messageId":"31fa1fb324be240e28bd360ee3afc45d8fb8444f.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 08/12] run-command: add pre-exec callback for child processes","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:56Z","receivedAt":"2026-03-05T00:52:16Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pre_exec_cb function pointer to struct child_process that is\ninvoked in the child between fork and exec.  This gives callers a\nplace to perform setup that must happen in the child's context,\nsuch as closing inherited file descriptors.\n\nProvide close_fd_above_stderr() as a ready-made callback that\ncloses file descriptors 3 and above (skipping the child-notifier\npipe), capped at sysconf(_SC_OPEN_MAX) or 4096, whichever is\nsmaller.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 15 +++++++++++++++\n run-command.h | 15 +++++++++++++++\n 2 files changed, 30 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..b9bc84ca1b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -674,6 +674,18 @@ static void trace_run_command(const struct child_process *cp)\n \tstrbuf_release(&buf);\n }\n \n+void close_fd_above_stderr(void)\n+{\n+\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\tint fd;\n+\tif (max_fd < 0 || max_fd > 4096)\n+\t\tmax_fd = 4096;\n+\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\tif (fd != child_notifier)\n+\t\t\tclose(fd);\n+\t}\n+}\n+\n int start_command(struct child_process *cmd)\n {\n \tint need_in, need_out, need_err;\n@@ -832,6 +844,9 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->pre_exec_cb)\n+\t\t\tcmd->pre_exec_cb();\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..7ea5c6e005 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,14 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * If set, the callback is invoked in the child between fork and\n+\t * exec.  It can be used, for example, to close inherited file\n+\t * descriptors that the child should not keep open.\n+\t */\n+\tvoid (*pre_exec_cb)(void);\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n@@ -149,6 +157,13 @@ struct child_process {\n \t.env = STRVEC_INIT, \\\n }\n \n+/**\n+ * Close file descriptors 3 and above.  Suitable for use as a\n+ * pre_exec_cb to prevent the child from inheriting pipe endpoints\n+ * or other descriptors from the parent environment.\n+ */\n+void close_fd_above_stderr(void);\n+\n /**\n  * The functions: start_command, finish_command, run_command do the following:\n  *\n-- \ngitgitgadget\n\n"},{"id":"537865","messageId":"c963074cbd8ae0e7da783ec0abd2996eea788823.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 09/12] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:57Z","receivedAt":"2026-03-05T00:52:18Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..d2f250bd06 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.pre_exec_cb = close_fd_above_stderr;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..61cb789339 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.pre_exec_cb = close_fd_above_stderr;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"537866","messageId":"ee3ee75c9418982ef365aa81e60116c4d2e4c266.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 10/12] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:58Z","receivedAt":"2026-03-05T00:52:19Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d2f250bd06..299de2e4e2 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"537867","messageId":"54bd8f604a1432df497d19e9a48e96393517d632.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 11/12] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:51:59Z","receivedAt":"2026-03-05T00:52:20Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"537868","messageId":"e603fc7ddeff63705da093fde0dc3687219b1ffc.1772671920.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v9 12/12] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:52:00Z","receivedAt":"2026-03-05T00:52:22Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 299de2e4e2..b4b2a304e5 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"537871","messageId":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v9.git.git.1772671920.gitgitgadget@gmail.com","subject":"[PATCH v10 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:06Z","receivedAt":"2026-03-05T01:16:22Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review\n * Provided close_fd_above_stderr() as a ready-made callback function\n * Fixed Windows build: close_fd_above_stderr() compiles as a no-op on\n   Windows since there is no fork/exec\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (12):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add pre-exec callback for child processes\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  18 +\n run-command.h                                 |  16 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n 18 files changed, 1275 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v10\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v10\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v9:\n\n  1:  4d4dec8fa1 =  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  2:  cb270120f0 =  2:  cb270120f0 fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  3:  44a063074d =  3:  44a063074d compat/win32: add pthread_cond_timedwait\n  4:  b1081d1e13 =  4:  b1081d1e13 fsmonitor: use pthread_cond_timedwait for cookie wait\n  5:  dec0fb144f =  5:  dec0fb144f fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  6:  b2aaadb4ae =  6:  b2aaadb4ae fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  7:  03cf12d01b =  7:  03cf12d01b fsmonitor: implement filesystem change listener for Linux\n  8:  31fa1fb324 !  8:  39dcfbb7c8 run-command: add pre-exec callback for child processes\n     @@ Commit message\n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n       ## run-command.c ##\n     -@@ run-command.c: static void trace_run_command(const struct child_process *cp)\n     - \tstrbuf_release(&buf);\n     +@@ run-command.c: static void atfork_parent(struct atfork_state *as)\n     + \t\t\"restoring signal mask\");\n     + #endif\n       }\n     ++\n     + #endif /* GIT_WINDOWS_NATIVE */\n       \n      +void close_fd_above_stderr(void)\n      +{\n     ++#ifndef GIT_WINDOWS_NATIVE\n      +\tlong max_fd = sysconf(_SC_OPEN_MAX);\n      +\tint fd;\n      +\tif (max_fd < 0 || max_fd > 4096)\n     @@ run-command.c: static void trace_run_command(const struct child_process *cp)\n      +\t\tif (fd != child_notifier)\n      +\t\t\tclose(fd);\n      +\t}\n     ++#endif\n      +}\n      +\n     - int start_command(struct child_process *cmd)\n     + static inline void set_cloexec(int fd)\n       {\n     - \tint need_in, need_out, need_err;\n     + \tint flags = fcntl(fd, F_GETFD);\n      @@ run-command.c: fail_pipe:\n       \t\t\tchild_close(cmd->out);\n       \t\t}\n     @@ run-command.h: struct child_process {\n      +/**\n      + * Close file descriptors 3 and above.  Suitable for use as a\n      + * pre_exec_cb to prevent the child from inheriting pipe endpoints\n     -+ * or other descriptors from the parent environment.\n     ++ * or other descriptors from the parent environment.  On Windows\n     ++ * this is a no-op since there is no fork/exec.\n      + */\n      +void close_fd_above_stderr(void);\n      +\n  9:  c963074cbd =  9:  5db0591c15 fsmonitor: close inherited file descriptors and detach in daemon\n 10:  ee3ee75c94 = 10:  8a9a6ba4fa fsmonitor: add timeout to daemon stop command\n 11:  54bd8f604a = 11:  27d5560007 fsmonitor: add tests for Linux\n 12:  e603fc7dde = 12:  8ea20aab4c fsmonitor: convert shown khash to strset in do_handle_client\n\n-- \ngitgitgadget\n"},{"id":"537872","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 01/12] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:07Z","receivedAt":"2026-03-05T01:16:23Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"537873","messageId":"cb270120f0e27a34a58c856b7c80e78e2301c989.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 02/12] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:08Z","receivedAt":"2026-03-05T01:16:25Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"537874","messageId":"44a063074dadc27110e2dd3481756c8c324ed956.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 03/12] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:09Z","receivedAt":"2026-03-05T01:16:27Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"537875","messageId":"b1081d1e1370bb9c9755d0635e7c7466c954f046.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 04/12] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:10Z","receivedAt":"2026-03-05T01:16:29Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"537876","messageId":"dec0fb144f9ea4efe8d15526b376f51ef470ed5d.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 05/12] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:11Z","receivedAt":"2026-03-05T01:16:31Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"537877","messageId":"b2aaadb4aea37729989c6cd08838d7aee5b9dc4c.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 06/12] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:12Z","receivedAt":"2026-03-05T01:16:33Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537879","messageId":"03cf12d01b8d75610b4c35ff412cfe6cd222a171.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 07/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:13Z","receivedAt":"2026-03-05T01:16:34Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"537878","messageId":"39dcfbb7c8f870d8c520772563f1a3b0380929e0.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 08/12] run-command: add pre-exec callback for child processes","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:14Z","receivedAt":"2026-03-05T01:16:35Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pre_exec_cb function pointer to struct child_process that is\ninvoked in the child between fork and exec.  This gives callers a\nplace to perform setup that must happen in the child's context,\nsuch as closing inherited file descriptors.\n\nProvide close_fd_above_stderr() as a ready-made callback that\ncloses file descriptors 3 and above (skipping the child-notifier\npipe), capped at sysconf(_SC_OPEN_MAX) or 4096, whichever is\nsmaller.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 18 ++++++++++++++++++\n run-command.h | 16 ++++++++++++++++\n 2 files changed, 34 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..cfbfe68b33 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -546,8 +546,23 @@ static void atfork_parent(struct atfork_state *as)\n \t\t\"restoring signal mask\");\n #endif\n }\n+\n #endif /* GIT_WINDOWS_NATIVE */\n \n+void close_fd_above_stderr(void)\n+{\n+#ifndef GIT_WINDOWS_NATIVE\n+\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\tint fd;\n+\tif (max_fd < 0 || max_fd > 4096)\n+\t\tmax_fd = 4096;\n+\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\tif (fd != child_notifier)\n+\t\t\tclose(fd);\n+\t}\n+#endif\n+}\n+\n static inline void set_cloexec(int fd)\n {\n \tint flags = fcntl(fd, F_GETFD);\n@@ -832,6 +847,9 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->pre_exec_cb)\n+\t\t\tcmd->pre_exec_cb();\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..375d2c731d 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,14 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * If set, the callback is invoked in the child between fork and\n+\t * exec.  It can be used, for example, to close inherited file\n+\t * descriptors that the child should not keep open.\n+\t */\n+\tvoid (*pre_exec_cb)(void);\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n@@ -149,6 +157,14 @@ struct child_process {\n \t.env = STRVEC_INIT, \\\n }\n \n+/**\n+ * Close file descriptors 3 and above.  Suitable for use as a\n+ * pre_exec_cb to prevent the child from inheriting pipe endpoints\n+ * or other descriptors from the parent environment.  On Windows\n+ * this is a no-op since there is no fork/exec.\n+ */\n+void close_fd_above_stderr(void);\n+\n /**\n  * The functions: start_command, finish_command, run_command do the following:\n  *\n-- \ngitgitgadget\n\n"},{"id":"537880","messageId":"5db0591c155e02e778eff0916af78c17cd7e488b.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 09/12] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:15Z","receivedAt":"2026-03-05T01:16:37Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..d2f250bd06 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.pre_exec_cb = close_fd_above_stderr;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..61cb789339 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.pre_exec_cb = close_fd_above_stderr;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"537881","messageId":"8a9a6ba4fad9dd21844ef12a9cf24c0f26c1bc53.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 10/12] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:16Z","receivedAt":"2026-03-05T01:16:38Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d2f250bd06..299de2e4e2 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"537882","messageId":"27d55600071d3479ffe2447ad36a5715d7eb2521.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 11/12] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:17Z","receivedAt":"2026-03-05T01:16:39Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"537884","messageId":"8ea20aab4c71454c5abebbecfdef451eb4146ed5.1772673378.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v10 12/12] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T01:16:18Z","receivedAt":"2026-03-05T01:16:40Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 299de2e4e2..b4b2a304e5 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"537887","messageId":"xmqqv7fa52x4.fsf@gitster.g","threadId":"64696","inReplyTo":"20260305004959.83647-1-github@paulisageek.com","subject":"Re: [PATCH v8 09/12] run-command: add close_fd_above_stderr option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-05T04:13:43Z","receivedAt":"2026-03-05T04:13:46Z","isPatch":true,"body":"Paul Tarjan <paul@paultarjan.com> writes:\n\n> On Wed, Mar 4, 2026, Junio C Hamano wrote:\n>> I wonder if a generic callback function\n>> to call here in the child between fork and exec that the caller can\n>> supply would be a good thing to have.\n>\n> Done in v9. Replaced the close_fd_above_stderr flag with a pre_exec_cb\n> function pointer on struct child_process. The fd-closing logic is now\n> a standalone close_fd_above_stderr() function that the two fsmonitor\n> callers pass as the callback.\n\nI didn't mean to suggest using a generic mechanism to _replace_ what\nyou added.\n\nA truly generic callback mechanism that will be useful can and\nshould wait until we see real use cases for one.\n\nAnd I strongly suspect that the callback would want to take some\ncallback data argument, not \"void cb(void)\", but more like \"int\ncb(void *)\" (we may find a return value that lets us tell the\nrun_command() to abort instead of exec(2)ingg, for example---and we\ncan make a better design when we do have real use cases.\n\n\n\n\n"},{"id":"537889","messageId":"20260305063858.37393-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqv7fa52x4.fsf@gitster.g","subject":"Re: [PATCH v9 09/12] run-command: add pre-exec callback for child processes","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-05T06:38:58Z","receivedAt":"2026-03-05T06:39:01Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I didn't mean to suggest using a generic mechanism to _replace_ what\n> you added.\n>\n> A truly generic callback mechanism that will be useful can and\n> should wait until we see real use cases for one.\n\nHaha, got it. I'll put it back and let you mull it over for another\npatch.\n"},{"id":"537891","messageId":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v10.git.git.1772673378.gitgitgadget@gmail.com","subject":"[PATCH v11 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:00Z","receivedAt":"2026-03-05T06:55:18Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v10:\n\n * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n   per Junio's clarification (same as v8)\n\nChanges since v9:\n\n * Fixed Windows build: close_fd_above_stderr() compiles as a no-op on\n   Windows since there is no fork/exec\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review, with close_fd_above_stderr() as\n   a ready-made callback\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (12):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  12 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n 18 files changed, 1262 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v11\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v11\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v10:\n\n  1:  4d4dec8fa1 =  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  2:  cb270120f0 =  2:  cb270120f0 fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  3:  44a063074d =  3:  44a063074d compat/win32: add pthread_cond_timedwait\n  4:  b1081d1e13 =  4:  b1081d1e13 fsmonitor: use pthread_cond_timedwait for cookie wait\n  5:  dec0fb144f =  5:  dec0fb144f fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  6:  b2aaadb4ae =  6:  b2aaadb4ae fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  7:  03cf12d01b =  7:  03cf12d01b fsmonitor: implement filesystem change listener for Linux\n  8:  39dcfbb7c8 !  8:  50f5b4676e run-command: add pre-exec callback for child processes\n     @@ Metadata\n      Author: Paul Tarjan <github@paulisageek.com>\n      \n       ## Commit message ##\n     -    run-command: add pre-exec callback for child processes\n     +    run-command: add close_fd_above_stderr option\n      \n     -    Add a pre_exec_cb function pointer to struct child_process that is\n     -    invoked in the child between fork and exec.  This gives callers a\n     -    place to perform setup that must happen in the child's context,\n     -    such as closing inherited file descriptors.\n     -\n     -    Provide close_fd_above_stderr() as a ready-made callback that\n     -    closes file descriptors 3 and above (skipping the child-notifier\n     -    pipe), capped at sysconf(_SC_OPEN_MAX) or 4096, whichever is\n     -    smaller.\n     +    Add a close_fd_above_stderr flag to struct child_process.  When set,\n     +    the child closes file descriptors 3 and above between fork and exec\n     +    (skipping the child-notifier pipe), capped at sysconf(_SC_OPEN_MAX)\n     +    or 4096, whichever is smaller.  This prevents the child from\n     +    inheriting pipe endpoints or other descriptors from the parent\n     +    environment (e.g., the test harness).\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n     @@ run-command.c: static void atfork_parent(struct atfork_state *as)\n      +\n       #endif /* GIT_WINDOWS_NATIVE */\n       \n     -+void close_fd_above_stderr(void)\n     -+{\n     -+#ifndef GIT_WINDOWS_NATIVE\n     -+\tlong max_fd = sysconf(_SC_OPEN_MAX);\n     -+\tint fd;\n     -+\tif (max_fd < 0 || max_fd > 4096)\n     -+\t\tmax_fd = 4096;\n     -+\tfor (fd = 3; fd < max_fd; fd++) {\n     -+\t\tif (fd != child_notifier)\n     -+\t\t\tclose(fd);\n     -+\t}\n     -+#endif\n     -+}\n     -+\n       static inline void set_cloexec(int fd)\n     - {\n     - \tint flags = fcntl(fd, F_GETFD);\n      @@ run-command.c: fail_pipe:\n       \t\t\tchild_close(cmd->out);\n       \t\t}\n       \n     -+\t\tif (cmd->pre_exec_cb)\n     -+\t\t\tcmd->pre_exec_cb();\n     ++\t\tif (cmd->close_fd_above_stderr) {\n     ++\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n     ++\t\t\tint fd;\n     ++\t\t\tif (max_fd < 0 || max_fd > 4096)\n     ++\t\t\t\tmax_fd = 4096;\n     ++\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n     ++\t\t\t\tif (fd != child_notifier)\n     ++\t\t\t\t\tclose(fd);\n     ++\t\t\t}\n     ++\t\t}\n      +\n       \t\tif (cmd->dir && chdir(cmd->dir))\n       \t\t\tchild_die(CHILD_ERR_CHDIR);\n     @@ run-command.h: struct child_process {\n       \tunsigned wait_after_clean:1;\n      +\n      +\t/**\n     -+\t * If set, the callback is invoked in the child between fork and\n     -+\t * exec.  It can be used, for example, to close inherited file\n     -+\t * descriptors that the child should not keep open.\n     ++\t * Close file descriptors 3 and above in the child after forking\n     ++\t * but before exec.  This prevents the child from inheriting\n     ++\t * pipe endpoints or other descriptors from the parent\n     ++\t * environment (e.g., the test harness).\n      +\t */\n     -+\tvoid (*pre_exec_cb)(void);\n     ++\tunsigned close_fd_above_stderr:1;\n      +\n       \tvoid (*clean_on_exit_handler)(struct child_process *process);\n       };\n       \n     -@@ run-command.h: struct child_process {\n     - \t.env = STRVEC_INIT, \\\n     - }\n     - \n     -+/**\n     -+ * Close file descriptors 3 and above.  Suitable for use as a\n     -+ * pre_exec_cb to prevent the child from inheriting pipe endpoints\n     -+ * or other descriptors from the parent environment.  On Windows\n     -+ * this is a no-op since there is no fork/exec.\n     -+ */\n     -+void close_fd_above_stderr(void);\n     -+\n     - /**\n     -  * The functions: start_command, finish_command, run_command do the following:\n     -  *\n  9:  5db0591c15 !  9:  057b3098bc fsmonitor: close inherited file descriptors and detach in daemon\n     @@ builtin/fsmonitor--daemon.c: static int try_to_start_background_daemon(void)\n       \tcp.no_stdin = 1;\n       \tcp.no_stdout = 1;\n       \tcp.no_stderr = 1;\n     -+\tcp.pre_exec_cb = close_fd_above_stderr;\n     ++\tcp.close_fd_above_stderr = 1;\n       \n       \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n       \t\t\t\tfsmonitor__start_timeout_sec);\n     @@ fsmonitor-ipc.c: static int spawn_daemon(void)\n       \tcmd.no_stdin = 1;\n      +\tcmd.no_stdout = 1;\n      +\tcmd.no_stderr = 1;\n     -+\tcmd.pre_exec_cb = close_fd_above_stderr;\n     ++\tcmd.close_fd_above_stderr = 1;\n       \tcmd.trace2_child_class = \"fsmonitor\";\n       \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n       \n 10:  8a9a6ba4fa = 10:  e6bc3bfcb2 fsmonitor: add timeout to daemon stop command\n 11:  27d5560007 = 11:  81f8cd1599 fsmonitor: add tests for Linux\n 12:  8ea20aab4c = 12:  8fa6a74e0d fsmonitor: convert shown khash to strset in do_handle_client\n\n-- \ngitgitgadget\n"},{"id":"537892","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 01/12] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:01Z","receivedAt":"2026-03-05T06:55:20Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"537893","messageId":"cb270120f0e27a34a58c856b7c80e78e2301c989.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 02/12] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:02Z","receivedAt":"2026-03-05T06:55:21Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"537894","messageId":"44a063074dadc27110e2dd3481756c8c324ed956.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 03/12] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:03Z","receivedAt":"2026-03-05T06:55:23Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"537895","messageId":"b1081d1e1370bb9c9755d0635e7c7466c954f046.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 04/12] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:04Z","receivedAt":"2026-03-05T06:55:25Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"537896","messageId":"dec0fb144f9ea4efe8d15526b376f51ef470ed5d.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 05/12] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:05Z","receivedAt":"2026-03-05T06:55:27Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"537897","messageId":"b2aaadb4aea37729989c6cd08838d7aee5b9dc4c.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 06/12] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:06Z","receivedAt":"2026-03-05T06:55:29Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"537898","messageId":"03cf12d01b8d75610b4c35ff412cfe6cd222a171.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 07/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:07Z","receivedAt":"2026-03-05T06:55:33Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"537899","messageId":"50f5b4676e531c944ea40eaad265b14c2467a808.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 08/12] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:08Z","receivedAt":"2026-03-05T06:55:35Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a close_fd_above_stderr flag to struct child_process.  When set,\nthe child closes file descriptors 3 and above between fork and exec\n(skipping the child-notifier pipe), capped at sysconf(_SC_OPEN_MAX)\nor 4096, whichever is smaller.  This prevents the child from\ninheriting pipe endpoints or other descriptors from the parent\nenvironment (e.g., the test harness).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 12 ++++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..f4361906c9 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -546,6 +546,7 @@ static void atfork_parent(struct atfork_state *as)\n \t\t\"restoring signal mask\");\n #endif\n }\n+\n #endif /* GIT_WINDOWS_NATIVE */\n \n static inline void set_cloexec(int fd)\n@@ -832,6 +833,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..fdaa01e140 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the child from inheriting\n+\t * pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"537900","messageId":"057b3098bcbea0302877bab0abbb9f9a21194f40.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 09/12] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:09Z","receivedAt":"2026-03-05T06:55:37Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..b2a816dc3f 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"537901","messageId":"e6bc3bfcb285b48bff804460943aef17f0e00b6d.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 10/12] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:10Z","receivedAt":"2026-03-05T06:55:38Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex b2a816dc3f..53d8ad1f0d 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"537902","messageId":"81f8cd159945463927dfd2f78645f55f23b9498c.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 11/12] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:11Z","receivedAt":"2026-03-05T06:55:40Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"537903","messageId":"8fa6a74e0df89452b25f8755fcd3290fcd8f1e2d.1772693712.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v11 12/12] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T06:55:12Z","receivedAt":"2026-03-05T06:55:43Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 53d8ad1f0d..f920cf3a82 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"537906","messageId":"aakyzGksiC2fhhiD@pks.im","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"Re: [PATCH v11 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-05T07:37:48Z","receivedAt":"2026-03-05T07:37:54Z","isPatch":true,"body":"On Thu, Mar 05, 2026 at 06:55:00AM +0000, Paul Tarjan via GitGitGadget wrote:\n> Changes since v10:\n> \n>  * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n>    per Junio's clarification (same as v8)\n\nOne ask from my side: I would welcome it if you slowed down a bit with\nsending out new versions. Sending three different versions within a\ncouple hours without any reviews in between makes me pause, as I have no\nidea whether the current version is good to be reviewed or whether I\nshould expect another 5 rerolls.\n\nSo please take a bit more time to work feedback into your patch series\nbefore sending out the next version. It's totally fine to wait a couple\ndays between iterations.\n\nAlso, could you please clarify whether the patch series has been written\nby AI and if so, which parts of it are?\n\nThanks!\n\nPatrick\n"},{"id":"537955","messageId":"20260305141525.98458-1-github@paulisageek.com","threadId":"64696","inReplyTo":"aakyzGksiC2fhhiD@pks.im","subject":"Re: [PATCH v11 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-05T14:15:25Z","receivedAt":"2026-03-05T14:15:30Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> One ask from my side: I would welcome it if you slowed down a bit with\n> sending out new versions. Sending three different versions within a\n> couple hours without any reviews in between makes me pause, as I have no\n> idea whether the current version is good to be reviewed or whether I\n> should expect another 5 rerolls.\n\nApologies for that. I thought integrating the feedback would be a quick\nfix since it was small and worked locally so I /submitted it without\nwaiting for a Windows build. When it broke I fixed the Windows version,\nsaw it passed and typed /submit. Then I got the feedback on the mailing\nlist that it wasn't what Junio wanted so I undid that and noted in my\nfront matter that (v11 == v8).\n\n> So please take a bit more time to work feedback into your patch series\n> before sending out the next version. It's totally fine to wait a couple\n> days between iterations.\n\nSorry for the quick submissions. I'm more used to the Github\ndevelopment flow where quick iteration on feedback is optimal to get a\nPR merged.\n\n> Also, could you please clarify whether the patch series has been written\n> by AI and if so, which parts of it are?\n\nI'm an Engineer at Anthropic so I've sadly completely lost my vim muscle\nthat I built up over 20 years. All the file fwrite() operations were\nexecuted by my Claude wrapper but fully directed and iterated by me,\nwith every line reviewed by me. That's just how I code nowadays. Claude\nis also an excellent code reviewer so it found multiple bugs in my code\ntoo. I'm aware of your policy and think I'm complying with it, but\nplease let me know if you prefer something else:\nhttps://git-scm.com/docs/SubmittingPatches#ai\n\nThese email replies are written by me but sent by Claude calling git\ncommands as I've messed up formatting in the past trying to use Gmail\nand couldn't figure out the right git incantation to send to the mailing\nlist, but Claude could.\n\n> Thanks!\n>\n> Patrick\n"},{"id":"540003","messageId":"xmqqwlyzsmq7.fsf@gitster.g","threadId":"64696","inReplyTo":"aakyzGksiC2fhhiD@pks.im","subject":"Re: [PATCH v11 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-25T20:00:48Z","receivedAt":"2026-03-25T20:00:50Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Thu, Mar 05, 2026 at 06:55:00AM +0000, Paul Tarjan via GitGitGadget wrote:\n>> Changes since v10:\n>> \n>>  * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n>>    per Junio's clarification (same as v8)\n>\n> One ask from my side: I would welcome it if you slowed down a bit with\n> sending out new versions. Sending three different versions within a\n> couple hours without any reviews in between makes me pause, as I have no\n> idea whether the current version is good to be reviewed or whether I\n> should expect another 5 rerolls.\n>\n> So please take a bit more time to work feedback into your patch series\n> before sending out the next version. It's totally fine to wait a couple\n> days between iterations.\n>\n> Also, could you please clarify whether the patch series has been written\n> by AI and if so, which parts of it are?\n>\n> Thanks!\n>\n> Patrick\n\nThe topic went totally silent after this message; is this topic\nstill viable?\n\nThanks.\n"},{"id":"540309","messageId":"20260329044748.99269-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqwlyzsmq7.fsf@gitster.g","subject":"Re: [PATCH v11 0/8] fsmonitor: add Linux support using inotify","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-29T04:47:47Z","receivedAt":"2026-03-29T04:47:50Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> The topic went totally silent after this message; is this topic\n> still viable?\n\nWere you asking me, or Patrick? On my end: yes, very much. I've\nbeen using this across our fleet for months and would love to see\nit upstream.\n\nI replied to Patrick on Mar 5 (slowed down, answered the AI\nquestion) and haven't sent a new version since. v11 is identical\nto v8.\n\nHappy to do whatever's needed. Just let me know who the ball is\nwith.\n\n(Also a small process question: I keep getting gitgitgadget emails\nabout the branch being integrated into \"seen\" every few days. Is\nthat just informational, or does it mean something I should act\non?)\n\nThanks\nPaul\n"},{"id":"540366","messageId":"acphGnG9uX9C6dvx@pks.im","threadId":"64696","inReplyTo":"xmqqwlyzsmq7.fsf@gitster.g","subject":"Re: [PATCH v11 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-30T11:40:10Z","receivedAt":"2026-03-30T11:40:17Z","isPatch":true,"body":"On Wed, Mar 25, 2026 at 01:00:48PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > On Thu, Mar 05, 2026 at 06:55:00AM +0000, Paul Tarjan via GitGitGadget wrote:\n> >> Changes since v10:\n> >> \n> >>  * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n> >>    per Junio's clarification (same as v8)\n> >\n> > One ask from my side: I would welcome it if you slowed down a bit with\n> > sending out new versions. Sending three different versions within a\n> > couple hours without any reviews in between makes me pause, as I have no\n> > idea whether the current version is good to be reviewed or whether I\n> > should expect another 5 rerolls.\n> >\n> > So please take a bit more time to work feedback into your patch series\n> > before sending out the next version. It's totally fine to wait a couple\n> > days between iterations.\n> >\n> > Also, could you please clarify whether the patch series has been written\n> > by AI and if so, which parts of it are?\n> >\n> > Thanks!\n> >\n> > Patrick\n> \n> The topic went totally silent after this message; is this topic\n> still viable?\n\nI didn't have the capacity recently to focus on this patch series.\n\nAlso, I think we need to be extra careful with merging this patch series\nas it's written mostly by AI as far as I understood. So I don't quite\nfeel comfortable to ACK this series by myself, as I'm not sure to what\ndegree the code is understood and reasoned about by anyone.\n\nI'm not against merging it in general, but if we want to merge it I\nwould like to have at least one more thorough review by somebody who\nunderstands this area before I feel comfortable committing to it and\nmaintaining the code going forward.\n\nThanks!\n\nPatrick\n"},{"id":"540453","messageId":"xmqqjyus4qp2.fsf@gitster.g","threadId":"64696","inReplyTo":"acphGnG9uX9C6dvx@pks.im","subject":"Re: [PATCH v11 00/12] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T03:34:49Z","receivedAt":"2026-03-31T03:34:52Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> > So please take a bit more time to work feedback into your patch series\n>> > before sending out the next version. It's totally fine to wait a couple\n>> > days between iterations.\n>> >\n>> > Also, could you please clarify whether the patch series has been written\n>> > by AI and if so, which parts of it are?\n>> >\n>> > Thanks!\n>> >\n>> > Patrick\n>> \n>> The topic went totally silent after this message; is this topic\n>> still viable?\n>\n> I didn't have the capacity recently to focus on this patch series.\n>\n> Also, I think we need to be extra careful with merging this patch series\n> as it's written mostly by AI as far as I understood. So I don't quite\n> feel comfortable to ACK this series by myself, as I'm not sure to what\n> degree the code is understood and reasoned about by anyone.\n>\n> I'm not against merging it in general, but if we want to merge it I\n> would like to have at least one more thorough review by somebody who\n> understands this area before I feel comfortable committing to it and\n> maintaining the code going forward.\n\nI share that exact feeling.  After all, the section recently added\nto SubmittingPatches about AI-generated code was to address this\nkind of patches.\n\nIn any case, when this topic is merged to 'seen', it seem to break\nlinux-TEST-vars CI job, which essentially runs the build and test\nwith these settings:\n\n        export OPENSSL_SHA1_UNSAFE=YesPlease\n        export GIT_TEST_SPLIT_INDEX=yes\n        export GIT_TEST_FULL_IN_PACK_ARRAY=true\n        export GIT_TEST_OE_SIZE=10\n        export GIT_TEST_OE_DELTA_SIZE=5\n        export GIT_TEST_COMMIT_GRAPH=1\n        export GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1\n        export GIT_TEST_MULTI_PACK_INDEX=1\n        export GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=1\n        export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master\n        export GIT_TEST_NO_WRITE_REV_INDEX=1\n        export GIT_TEST_CHECKOUT_WORKERS=2\n        export GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL=1\n\nand t9210 fails with this topic merged (abfd972), and succeeds\nwithout (4b8355c6f8).\n\n  https://github.com/git/git/actions/runs/23775508256/job/69276399578#step:10:562\n  https://github.com/git/git/actions/runs/23777158230/job/69281498657    \n\nI also reproduced the breakage locally on a debian-testing derivative.\n\nHelp to diagnose and pinpoint the bad interactions with topics in\nflight is greatly appreciated.\n\n\n! [abfd972] Merge branch 'pt/fsmonitor-linux' into seen\n ! [4b8355c6f8] Merge branch 'ps/receive-pack-updateinstead-in-worktree' into seen\n--\n-  [abfd972] Merge branch 'pt/fsmonitor-linux' into seen\n+  [abfd972^2] fsmonitor: convert shown khash to strset in do_handle_client\n+  [abfd972^2^] fsmonitor: add tests for Linux\n+  [abfd972^2~2] fsmonitor: add timeout to daemon stop command\n+  [abfd972^2~3] fsmonitor: close inherited file descriptors and detach in daemon\n+  [abfd972^2~4] run-command: add close_fd_above_stderr option\n+  [abfd972^2~5] fsmonitor: implement filesystem change listener for Linux\n+  [abfd972^2~6] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n+  [abfd972^2~7] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n+  [abfd972^2~8] fsmonitor: use pthread_cond_timedwait for cookie wait\n+  [abfd972^2~9] compat/win32: add pthread_cond_timedwait\n+  [abfd972^2~10] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n+  [abfd972^2~11] fsmonitor: fix khash memory leak in do_handle_client\n-- [4b8355c6f8] Merge branch 'ps/receive-pack-updateinstead-in-worktree' into seen\n\n\n\n"},{"id":"540468","messageId":"20260331061754.83335-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqjyus4qp2.fsf@gitster.g","subject":"Re: [PATCH v11 0/8] fsmonitor: add Linux support using inotify","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-03-31T06:17:54Z","receivedAt":"2026-03-31T06:17:56Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> In any case, when this topic is merged to 'seen', it seem to break\n> linux-TEST-vars CI job, which essentially runs the build and test\n> with these settings:\n>\n>         export GIT_TEST_SPLIT_INDEX=yes\n>         [...]\n>\n> and t9210 fails with this topic merged (abfd972), and succeeds\n> without (4b8355c6f8).\n\nThanks for the pointer. The failure is:\n\n  BUG: fsmonitor.c:27: fsmonitor_dirty has more entries than the\n  index (2 > 0)\n\nIt's a pre-existing bug in the interaction between fsmonitor and\nsplit-index, now exposed because fsmonitor-linux enables the\ndaemon on Linux for the first time (so scalar clone actually\nstarts it). It needs the interaction with\nps/setup-wo-the-repository in seen to trigger.\n\nThis is related to the issue that 05f28e4b3c (\"scalar: use\nindex.skipHash=true for performance\", 2025-06-04) worked around\nby disabling GIT_TEST_SPLIT_INDEX in t9210, noting \"the issue\nshould be resolved in a series focused on the split index.\" My\nfix covers the fsmonitor bitmap side; the index.skipHash\ninteraction remains.\n\nThe fsmonitor_dirty EWAH bitmap can reference positions from a\nprevious index state. With split-index, cache_nr can be smaller\nthan the bitmap expects. Two places hit this:\n\n1. tweak_fsmonitor() calls assert_index_minimum() without the\n   !istate->split_index guard that the read path (line 98) and\n   write path (line 128) already have.\n\n2. fsmonitor_ewah_callback() unconditionally asserts and then\n   accesses istate->cache[pos], which is out of bounds when\n   split-index hasn't merged all entries yet.\n\nI reproduced this in a Fedora container by merging fsmonitor-linux\ninto seen and running t9210 with GIT_TEST_SPLIT_INDEX=yes. Fails\nwithout the fix, passes with it.\n\nFix is pushed as a new commit at the end of the series:\n\n  fsmonitor_ewah_callback():\n  -  assert_index_minimum(istate, pos + 1);\n  +  if (pos >= istate->cache_nr)\n  +      return;\n\n  tweak_fsmonitor():\n  -  assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n  +  if (!istate->split_index)\n  +      assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n\nPaul\n"},{"id":"540469","messageId":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v11.git.git.1772693712.gitgitgadget@gmail.com","subject":"[PATCH v12 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:05Z","receivedAt":"2026-03-31T06:19:23Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v11:\n\n * Fix t9210 BUG assertion with GIT_TEST_SPLIT_INDEX=yes: guard\n   fsmonitor_dirty bitmap access against split-index having fewer entries\n   than the bitmap expects\n\nChanges since v10:\n\n * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n   per Junio's clarification (same as v8)\n\nChanges since v9:\n\n * Fixed Windows build: close_fd_above_stderr() compiles as a no-op on\n   Windows since there is no fork/exec\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review, with close_fd_above_stderr() as\n   a ready-made callback\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (13):\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n  fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n fsmonitor.c                                   |   6 +-\n meson.build                                   |  13 +-\n run-command.c                                 |  12 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n 19 files changed, 1266 insertions(+), 65 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v12\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v12\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v11:\n\n  1:  4d4dec8fa1 =  1:  4d4dec8fa1 fsmonitor: fix khash memory leak in do_handle_client\n  2:  cb270120f0 =  2:  cb270120f0 fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  3:  44a063074d =  3:  44a063074d compat/win32: add pthread_cond_timedwait\n  4:  b1081d1e13 =  4:  b1081d1e13 fsmonitor: use pthread_cond_timedwait for cookie wait\n  5:  dec0fb144f =  5:  dec0fb144f fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  6:  b2aaadb4ae =  6:  b2aaadb4ae fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  7:  03cf12d01b =  7:  03cf12d01b fsmonitor: implement filesystem change listener for Linux\n  8:  50f5b4676e =  8:  50f5b4676e run-command: add close_fd_above_stderr option\n  9:  057b3098bc =  9:  057b3098bc fsmonitor: close inherited file descriptors and detach in daemon\n 10:  e6bc3bfcb2 = 10:  e6bc3bfcb2 fsmonitor: add timeout to daemon stop command\n 11:  81f8cd1599 = 11:  81f8cd1599 fsmonitor: add tests for Linux\n 12:  8fa6a74e0d = 12:  8fa6a74e0d fsmonitor: convert shown khash to strset in do_handle_client\n  -:  ---------- > 13:  84ddbb30bb fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()\n\n-- \ngitgitgadget\n"},{"id":"540470","messageId":"4d4dec8fa161926e6f6ac822aff0db35353705eb.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 01/13] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:06Z","receivedAt":"2026-03-31T06:19:25Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"540471","messageId":"cb270120f0e27a34a58c856b7c80e78e2301c989.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 02/13] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:07Z","receivedAt":"2026-03-31T06:19:28Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"540472","messageId":"44a063074dadc27110e2dd3481756c8c324ed956.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 03/13] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:08Z","receivedAt":"2026-03-31T06:19:29Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"540473","messageId":"b1081d1e1370bb9c9755d0635e7c7466c954f046.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 04/13] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:09Z","receivedAt":"2026-03-31T06:19:33Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"540474","messageId":"dec0fb144f9ea4efe8d15526b376f51ef470ed5d.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 05/13] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:10Z","receivedAt":"2026-03-31T06:19:35Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"540475","messageId":"b2aaadb4aea37729989c6cd08838d7aee5b9dc4c.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 06/13] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:11Z","receivedAt":"2026-03-31T06:19:37Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"540476","messageId":"03cf12d01b8d75610b4c35ff412cfe6cd222a171.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 07/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:12Z","receivedAt":"2026-03-31T06:19:39Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"540477","messageId":"50f5b4676e531c944ea40eaad265b14c2467a808.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 08/13] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:13Z","receivedAt":"2026-03-31T06:19:41Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a close_fd_above_stderr flag to struct child_process.  When set,\nthe child closes file descriptors 3 and above between fork and exec\n(skipping the child-notifier pipe), capped at sysconf(_SC_OPEN_MAX)\nor 4096, whichever is smaller.  This prevents the child from\ninheriting pipe endpoints or other descriptors from the parent\nenvironment (e.g., the test harness).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 12 ++++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..f4361906c9 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -546,6 +546,7 @@ static void atfork_parent(struct atfork_state *as)\n \t\t\"restoring signal mask\");\n #endif\n }\n+\n #endif /* GIT_WINDOWS_NATIVE */\n \n static inline void set_cloexec(int fd)\n@@ -832,6 +833,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..fdaa01e140 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the child from inheriting\n+\t * pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"540478","messageId":"057b3098bcbea0302877bab0abbb9f9a21194f40.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 09/13] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:14Z","receivedAt":"2026-03-31T06:19:45Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..b2a816dc3f 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"540479","messageId":"e6bc3bfcb285b48bff804460943aef17f0e00b6d.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 10/13] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:15Z","receivedAt":"2026-03-31T06:19:47Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex b2a816dc3f..53d8ad1f0d 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"540480","messageId":"81f8cd159945463927dfd2f78645f55f23b9498c.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 11/13] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:16Z","receivedAt":"2026-03-31T06:19:48Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"540481","messageId":"8fa6a74e0df89452b25f8755fcd3290fcd8f1e2d.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 12/13] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:17Z","receivedAt":"2026-03-31T06:19:51Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 53d8ad1f0d..f920cf3a82 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"540482","messageId":"84ddbb30bb3862d4230ba1775d4c061832f2623f.1774937958.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-31T06:19:18Z","receivedAt":"2026-03-31T06:19:52Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen GIT_TEST_SPLIT_INDEX=yes is set and the fsmonitor daemon is\nactive, tweak_fsmonitor() can hit a BUG() assertion:\n\n  BUG: fsmonitor.c:27: fsmonitor_dirty has more entries than the index (2 > 0)\n\nThe fsmonitor_dirty EWAH bitmap may reference positions from a\nprevious index state.  With split-index, cache_nr can be smaller\nthan the bitmap expects because entries have not been merged yet.\n\nThis is related to the issue that 05f28e4b3c (scalar: use\nindex.skipHash=true for performance, 2025-06-04) worked around by\ndisabling GIT_TEST_SPLIT_INDEX in t9210, noting \"the issue should\nbe resolved in a series focused on the split index.\"  This fixes\nthe fsmonitor bitmap side; the index.skipHash interaction remains.\n\nTwo places hit this:\n\n  - tweak_fsmonitor() calls assert_index_minimum() without the\n    !istate->split_index guard that the read path (line 98) and\n    write path (line 128) already have.  Add the same guard.\n\n  - fsmonitor_ewah_callback() unconditionally asserts and then\n    accesses istate->cache[pos], which is out of bounds with\n    split-index.  Replace the assertion with a bounds check that\n    silently skips positions beyond cache_nr.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n fsmonitor.c | 6 ++++--\n 1 file changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/fsmonitor.c b/fsmonitor.c\nindex d07dc18967..5e5a4fadea 100644\n--- a/fsmonitor.c\n+++ b/fsmonitor.c\n@@ -33,7 +33,8 @@ static void fsmonitor_ewah_callback(size_t pos, void *is)\n \tstruct index_state *istate = (struct index_state *)is;\n \tstruct cache_entry *ce;\n \n-\tassert_index_minimum(istate, pos + 1);\n+\tif (pos >= istate->cache_nr)\n+\t\treturn;\n \n \tce = istate->cache[pos];\n \tce->ce_flags &= ~CE_FSMONITOR_VALID;\n@@ -805,7 +806,8 @@ void tweak_fsmonitor(struct index_state *istate)\n \t\t\t}\n \n \t\t\t/* Mark all previously saved entries as dirty */\n-\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n+\t\t\tif (!istate->split_index)\n+\t\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n \t\t\tewah_each_bit(istate->fsmonitor_dirty, fsmonitor_ewah_callback, istate);\n \n \t\t\trefresh_fsmonitor(istate);\n-- \ngitgitgadget\n"},{"id":"540532","messageId":"xmqqikac2dcq.fsf@gitster.g","threadId":"64696","inReplyTo":"20260331061754.83335-1-github@paulisageek.com","subject":"Re: [PATCH v11 0/8] fsmonitor: add Linux support using inotify","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T16:05:57Z","receivedAt":"2026-03-31T16:05:59Z","isPatch":true,"body":"Paul Tarjan <paul@paultarjan.com> writes:\n\n> I reproduced this in a Fedora container by merging fsmonitor-linux\n> into seen and running t9210 with GIT_TEST_SPLIT_INDEX=yes. Fails\n> without the fix, passes with it.\n>\n> Fix is pushed as a new commit at the end of the series:\n>\n>   fsmonitor_ewah_callback():\n>   -  assert_index_minimum(istate, pos + 1);\n>   +  if (pos >= istate->cache_nr)\n>   +      return;\n>\n>   tweak_fsmonitor():\n>   -  assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n>   +  if (!istate->split_index)\n>   +      assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n>\n> Paul\n\nThanks for a quick turnaround.\n\nIf it is a preexisting breakage in the original and not what you\nintroduced, wouldn't it make sense to apply the fix very early in\nthe series of patches, instead of patching breakage on top?  If we\nqueue on top of what we have in 'seen', this topic will still be\nbroken up to [8/9] in the same place, breaking bisectability, and\nfixed only when the last patch is applied.\n"},{"id":"540536","messageId":"xmqqse9g0xbx.fsf@gitster.g","threadId":"64696","inReplyTo":"84ddbb30bb3862d4230ba1775d4c061832f2623f.1774937958.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T16:37:22Z","receivedAt":"2026-03-31T16:37:25Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> When GIT_TEST_SPLIT_INDEX=yes is set and the fsmonitor daemon is\n> active, tweak_fsmonitor() can hit a BUG() assertion:\n>\n>   BUG: fsmonitor.c:27: fsmonitor_dirty has more entries than the index (2 > 0)\n>\n> The fsmonitor_dirty EWAH bitmap may reference positions from a\n> previous index state.  With split-index, cache_nr can be smaller\n> than the bitmap expects because entries have not been merged yet.\n>\n> This is related to the issue that 05f28e4b3c (scalar: use\n> index.skipHash=true for performance, 2025-06-04) worked around by\n> disabling GIT_TEST_SPLIT_INDEX in t9210, noting \"the issue should\n> be resolved in a series focused on the split index.\"  This fixes\n> the fsmonitor bitmap side; the index.skipHash interaction remains.\n\nLet's ask for input from the author of that scalar commit 05f28e4b3c,\nwhich hints that the fix should come with the focus on split-index.\n\nI also have to wonder if we should bury the issue like this patch\ndoes, which is not limited to the split case (i.e., the assert\nshould trigger if the requirement is not met while split-index is\nnot active, but the patch castrates it), instead of mimicking what\nDerrick did to work it around on the test side, leaving the\nproduction code still broken, which will give us better feel on the\nurgency of the split-index case in the real world.  I dunno.\n\n> Two places hit this:\n>\n>   - tweak_fsmonitor() calls assert_index_minimum() without the\n>     !istate->split_index guard that the read path (line 98) and\n>     write path (line 128) already have.  Add the same guard.\n>\n>   - fsmonitor_ewah_callback() unconditionally asserts and then\n>     accesses istate->cache[pos], which is out of bounds with\n>     split-index.  Replace the assertion with a bounds check that\n>     silently skips positions beyond cache_nr.\n>\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  fsmonitor.c | 6 ++++--\n>  1 file changed, 4 insertions(+), 2 deletions(-)\n>\n> diff --git a/fsmonitor.c b/fsmonitor.c\n> index d07dc18967..5e5a4fadea 100644\n> --- a/fsmonitor.c\n> +++ b/fsmonitor.c\n> @@ -33,7 +33,8 @@ static void fsmonitor_ewah_callback(size_t pos, void *is)\n>  \tstruct index_state *istate = (struct index_state *)is;\n>  \tstruct cache_entry *ce;\n>  \n> -\tassert_index_minimum(istate, pos + 1);\n> +\tif (pos >= istate->cache_nr)\n> +\t\treturn;\n>  \n>  \tce = istate->cache[pos];\n>  \tce->ce_flags &= ~CE_FSMONITOR_VALID;\n> @@ -805,7 +806,8 @@ void tweak_fsmonitor(struct index_state *istate)\n>  \t\t\t}\n>  \n>  \t\t\t/* Mark all previously saved entries as dirty */\n> -\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n> +\t\t\tif (!istate->split_index)\n> +\t\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n>  \t\t\tewah_each_bit(istate->fsmonitor_dirty, fsmonitor_ewah_callback, istate);\n>  \n>  \t\t\trefresh_fsmonitor(istate);\n"},{"id":"540543","messageId":"xmqqfr5f289u.fsf@gitster.g","threadId":"64696","inReplyTo":"xmqqse9g0xbx.fsf@gitster.g","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T17:55:41Z","receivedAt":"2026-03-31T17:55:44Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I also have to wonder if we should bury the issue like this patch\n> does, which is not limited to the split case (i.e., the assert\n> should trigger if the requirement is not met while split-index is\n> not active, but the patch castrates it), instead of mimicking what\n> Derrick did to work it around on the test side, leaving the\n> production code still broken, which will give us better feel on the\n> urgency of the split-index case in the real world.  I dunno.\n>\n>> Two places hit this:\n>>\n>>   - tweak_fsmonitor() calls assert_index_minimum() without the\n>>     !istate->split_index guard that the read path (line 98) and\n>>     write path (line 128) already have.  Add the same guard.\n>>\n>>   - fsmonitor_ewah_callback() unconditionally asserts and then\n>>     accesses istate->cache[pos], which is out of bounds with\n>>     split-index.  Replace the assertion with a bounds check that\n>>     silently skips positions beyond cache_nr.\n>>\n>> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n>> ---\n>>  fsmonitor.c | 6 ++++--\n>>  1 file changed, 4 insertions(+), 2 deletions(-)\n\nLet me ask the question differently.\n\n>> diff --git a/fsmonitor.c b/fsmonitor.c\n>> index d07dc18967..5e5a4fadea 100644\n>> --- a/fsmonitor.c\n>> +++ b/fsmonitor.c\n>> @@ -33,7 +33,8 @@ static void fsmonitor_ewah_callback(size_t pos, void *is)\n>>  \tstruct index_state *istate = (struct index_state *)is;\n>>  \tstruct cache_entry *ce;\n>>  \n>> -\tassert_index_minimum(istate, pos + 1);\n>> +\tif (pos >= istate->cache_nr)\n>> +\t\treturn;\n\nWhile this may avoid this \"assert\" stopping the process, what is the\nimplication of returning from here?  Who are the callers, what did\nthe callers want us to do to the cache entry at the \"pos\", and what\nhappens to their expectations if we simply returned to them without\ndoing anything they asked us to do?\n\n>> @@ -805,7 +806,8 @@ void tweak_fsmonitor(struct index_state *istate)\n>>  \t\t\t}\n>>  \n>>  \t\t\t/* Mark all previously saved entries as dirty */\n>> -\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n>> +\t\t\tif (!istate->split_index)\n>> +\t\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n\nThis part does make sense; the minimum may be relevant only when\nsplit-index is in effect, so we limit the check to non split case.\n\nI do have to wonder if there are some invariants we can/want to\nenforce here in the split case, but I do not mind punting it to a\nfix that is more focused on the split-index theme, as Derrick hinted\nin the earlier commit you found.\n\n>>  \t\t\tewah_each_bit(istate->fsmonitor_dirty, fsmonitor_ewah_callback, istate);\n>>  \n>>  \t\t\trefresh_fsmonitor(istate);\n\nThanks.\n"},{"id":"540615","messageId":"20260401041945.86738-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqfr5f289u.fsf@gitster.g","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-04-01T04:19:45Z","receivedAt":"2026-04-01T04:19:48Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> While this may avoid this \"assert\" stopping the process, what is the\n> implication of returning from here?  Who are the callers, what did\n> the callers want us to do to the cache entry at the \"pos\", and what\n> happens to their expectations if we simply returned to them without\n> doing anything they asked us to do?\n\nThe callback's job is to clear CE_FSMONITOR_VALID on the entry at\npos (marking it dirty). The only caller is tweak_fsmonitor() via\newah_each_bit().\n\nIn practice this guard shouldn't fire during normal operation.\nBy the time tweak_fsmonitor() runs, merge_base_index() has already\nmerged the split-index entries, so cache_nr is the full merged\nsize and the bitmap positions are all in range. The call order in\nread_index_from() is:\n\n  do_read_index()           -- reads split index, sets fsmonitor_dirty\n  do_read_index(base)       -- reads shared index\n  merge_base_index()        -- merges base into istate, cache_nr grows\n  post_read_index_from()    -- calls tweak_split_index, then tweak_fsmonitor\n\nThe guard only fires if the bitmap references positions that no\nlonger exist (e.g., entries were removed between write and read).\nIn that case returning is correct -- there's no entry to mark\ndirty.\n\nAnd even if we somehow missed marking something, refresh_fsmonitor()\nruns right after (line 813) and re-queries the daemon by pathname,\nso it catches any changes independently of the bitmap.\n\nThat said, if you'd rather keep the BUG() assertion as a canary\nand handle this on the test side like Derrick did, I'm fine with\ndropping the callback change and just keeping the\n!istate->split_index guard in tweak_fsmonitor().\n\n> This part does make sense; the minimum may be relevant only when\n> split-index is in effect, so we limit the check to non split case.\n\nRight.\n\n> I do have to wonder if there are some invariants we can/want to\n> enforce here in the split case, but I do not mind punting it to a\n> fix that is more focused on the split-index theme, as Derrick hinted\n> in the earlier commit you found.\n\nAgreed. The bitmap is written against the full merged index and\nread back before the merge, so during the read phase the positions\ncan legitimately exceed the split cache_nr. After the merge they\nshould all be in range. The read and write paths already punt on\nthis with the !istate->split_index guard; enforcing anything\ntighter probably belongs in a split-index focused series.\n\nI've also moved this commit to 1/13 per your earlier feedback\nabout bisectability.\n\nPaul\n"},{"id":"540934","messageId":"b96ed977-525e-c3fc-a626-db1a4b3da376@gmx.de","threadId":"64696","inReplyTo":"84ddbb30bb3862d4230ba1775d4c061832f2623f.1774937958.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2026-04-04T22:10:36Z","receivedAt":"2026-04-04T22:10:48Z","isPatch":true,"body":"Hi Paul,\n\nOn Tue, 31 Mar 2026, Paul Tarjan via GitGitGadget wrote:\n\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> When GIT_TEST_SPLIT_INDEX=yes is set and the fsmonitor daemon is\n> active, tweak_fsmonitor() can hit a BUG() assertion:\n> \n>   BUG: fsmonitor.c:27: fsmonitor_dirty has more entries than the index (2 > 0)\n> \n> The fsmonitor_dirty EWAH bitmap may reference positions from a\n> previous index state.  With split-index, cache_nr can be smaller\n> than the bitmap expects because entries have not been merged yet.\n> \n> This is related to the issue that 05f28e4b3c (scalar: use\n> index.skipHash=true for performance, 2025-06-04) worked around by\n> disabling GIT_TEST_SPLIT_INDEX in t9210, noting \"the issue should\n> be resolved in a series focused on the split index.\"  This fixes\n> the fsmonitor bitmap side; the index.skipHash interaction remains.\n> \n> Two places hit this:\n> \n>   - tweak_fsmonitor() calls assert_index_minimum() without the\n>     !istate->split_index guard that the read path (line 98) and\n>     write path (line 128) already have.  Add the same guard.\n> \n>   - fsmonitor_ewah_callback() unconditionally asserts and then\n>     accesses istate->cache[pos], which is out of bounds with\n>     split-index.  Replace the assertion with a bounds check that\n>     silently skips positions beyond cache_nr.\n\nThis issue (and in particular the BUG assertion reported by the CI\nfailures Junio pointed at in\nhttps://lore.kernel.org/git/xmqqjyus4qp2.fsf@gitster.g/) sounded quite\nfamiliar to me, so I consulted my notes. I had fixed a similar symptom\nafter a seriously tedious debugging session via 3b7a4475b091 (split-index;\nstop abusing the `base_oid` to strip the \"link\" extension, 2023-03-26).\nBut alas, this reference did not help, I found no similar bug that could\nbe the culprit here.\n\nSo I bisected the CI failure and it turns out that, as I suspected, this\npatch addresses a symptom, not the root cause. The bug has nothing to do\nwith Linux FSMonitor support and this patch should be dropped from the\nseries. See below for a suggestion what to do instead.\n\nThe failing CI job is `linux-TEST-vars`, which is the _only_ job that sets\n`GIT_TEST_SPLIT_INDEX=yes`. Before this series,\n`fsmonitor_ipc__is_supported()` returned 0 on Linux, so Scalar never set\n`core.fsmonitor=true`, and therefore there was never a FSMonitor bitmap in\nthe index, and the assertion in `tweak_fsmonitor()` could never fire. Now\nthat Linux has FSMonitor support, Scalar enables it, and the bitmap sanity\ncheck exposes a _pre-existing_ corruption in the `index.skipHash` +\nsplit-index interaction.\n\nThe interacting topic is `hn/git-checkout-m-with-stash` (specifically\nced477a20fa4, \"checkout: -m (--merge) uses autostash when switching\nbranches\"). That commit adds an unconditional `discard_index()` +\n`repo_read_index()` cycle at the end of `switch_branches()`, even when\nno autostash was created. (That is probably undesirable behavior, it\nshould be guarded.)\n\nHere is the chain of events:\n\n1. Scalar sets `index.skipHash=true` (this is Scalar's default since\n   05f28e4b3cc1 (scalar: use index.skipHash=true for performance,\n   2025-12-12)).\n\n2. With `GIT_TEST_SPLIT_INDEX=yes`, `write_locked_index()` writes a\n   split index. It calls `write_shared_index()`, which calls\n   `do_write_index()` for the shared index. Because `skipHash=true`,\n   the trailing hash is left as all-zeros, so `si->base->oid` is the\n   null OID.\n\n3. `write_shared_index()` then does\n   (https://gitlab.com/git-scm/git/-/blob/v2.53.0/read-cache.c#L3283):\n\n       oidcpy(&si->base_oid, &si->base->oid);\n\n   ...copying the null OID into `base_oid`. The shared index file is\n   renamed to `sharedindex.0000000000000000000000000000000000000000`.\n\n4. The `hn/git-checkout-m-with-stash` topic's unconditional\n   `discard_index()` + `repo_read_index()` re-reads the index from\n   disk.\n\n5. In `read_index_from()` (read-cache.c:2375), the condition\n\n       if (!split_index || is_null_oid(&split_index->base_oid))\n\n   is true, so the shared index is never loaded, `merge_base_index()`\n   is never called, and we jump straight to `post_read_index_from()`.\n\n6. This leaves `cache_nr=0` (all entries live in the never-loaded\n   shared index), but the FSMonitor extension's `fsmonitor_dirty`\n   bitmap was written against the full merged index (2 entries).\n\n7. `tweak_fsmonitor()` (called from `post_read_index_from()`) hits:\n\n       assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n\n   and fires the BUG: `bit_size=2 > cache_nr=0`.\n\nThe same bug would reproduce on macOS or Windows if those platforms had\na TEST-vars CI job with `GIT_TEST_SPLIT_INDEX=yes`.\n\nNow, about the patch itself (reordering the hunks for clarity):\n\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  fsmonitor.c | 6 ++++--\n>  1 file changed, 4 insertions(+), 2 deletions(-)\n> \n> diff --git a/fsmonitor.c b/fsmonitor.c\n> index d07dc18967..5e5a4fadea 100644\n> --- a/fsmonitor.c\n> +++ b/fsmonitor.c\n> @@ -805,7 +806,8 @@ void tweak_fsmonitor(struct index_state *istate)\n>  \t\t\t}\n>  \n>  \t\t\t/* Mark all previously saved entries as dirty */\n> -\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n> +\t\t\tif (!istate->split_index)\n> +\t\t\t\tassert_index_minimum(istate, istate->fsmonitor_dirty->bit_size);\n>  \t\t\tewah_each_bit(istate->fsmonitor_dirty, fsmonitor_ewah_callback, istate);\n>  \n>  \t\t\trefresh_fsmonitor(istate);\n\nThis guard in `tweak_fsmonitor()` is not analogous to the guards in\n`read_fsmonitor_extension()` and `write_fsmonitor_extension()`. Those\nguards exist because those functions run before `merge_base_index()` has\nbeen called. But `tweak_fsmonitor()` runs after `merge_base_index()` in\nthe normal read path (via `post_read_index_from()`). The fact that\n`cache_nr=0` when `tweak_fsmonitor()` runs means the shared index was\nnever loaded at all, which is the real bug.\n\nFor historical context: ba1b9caca699 (\"fsmonitor: delay updating state\nuntil after split index is merged\", 2017-10-27) intentionally arranged for\n`tweak_fsmonitor()` to run post-merge, and 392d797e2e60 (fsmonitor: do not\ncheck fsmonitor_dirty bits against cache entries on split index,\n2019-10-31) and 7621a6f43428 (fsmonitor: do not compare bitmap size with\nsize of split index, 2019-11-21) added the `!istate->split_index` guard to\nthe read/write paths only, deliberately leaving `tweak_fsmonitor()`\nunguarded because at that point the merge is expected to have happened.\ncae70acf2431 (fsmonitor: de-duplicate BUG()s around dirty bits,\n2021-01-23) refactored the BUG()s into `assert_index_minimum()`, again\npreserving this pattern. Suppressing the assertion here would hide the\nfact that the index is in a broken state.\n\n> @@ -33,7 +33,8 @@ static void fsmonitor_ewah_callback(size_t pos, void *is)\n>  \tstruct index_state *istate = (struct index_state *)is;\n>  \tstruct cache_entry *ce;\n>  \n> -\tassert_index_minimum(istate, pos + 1);\n> +\tif (pos >= istate->cache_nr)\n> +\t\treturn;\n>  \n>  \tce = istate->cache[pos];\n>  \tce->ce_flags &= ~CE_FSMONITOR_VALID;\n\nThis change to `fsmonitor_ewah_callback()` is particularly dangerous:\nsilently skipping dirty entries means files that the FSMonitor daemon\nreported as changed could be treated as unchanged. That is a correctness\nbug.\n\nThe proper fix belongs in one of two places:\n\n1. `write_shared_index()` should not allow `base_oid` to become the\n   null OID. When `index.skipHash=true`, the shared index needs a real\n   identifier. This is the root cause.\n\n2. Alternatively, the `hn/git-checkout-m-with-stash` topic should\n   guard its `discard_index()` + `repo_read_index()` with something\n   like `if (created_autostash)`, since the index reload is only\n   needed when an autostash was actually applied.\n\n   But that would probably only buy some time until another code path\n   introduces a similar pattern, and maybe shouldn't even be considered a\n   real fix.\n\nAlternatively, I'd suggest:\n\n3. The same work-around as was introduced in 05f28e4b3cc1 (scalar: use\n   index.skipHash=true for performance, 2025-12-12), namely to simply\n   force-disable split index in the offending test case (or move the\n   existing `sane_unset` to the top of the file, since\n   `index.skipHash=true` is a Scalar thing):\n\n  -- snip --\n  diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh\n  index 009437a5f316..a4e38ad644e4 100755\n  --- a/t/t9210-scalar.sh\n  +++ b/t/t9210-scalar.sh\n  @@ -152,6 +152,11 @@ test_expect_success 'set up repository to clone' '\n   '\n   \n   test_expect_success 'scalar clone' '\n  +\t# The split index refers to the base index via OID; Scalar sets\n  +\t# index.skipHash, though, and therefore that OID is always bogus;\n  +\t# Scalar/index.skipHash are simply incompatible with split-index\n  +\tsane_unset GIT_TEST_SPLIT_INDEX &&\n  +\n   \tsecond=$(git rev-parse --verify second:second.t) &&\n   \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n   \t(\n  -- snap --\n\nEither way, this 13/13 patch should be dropped from the fsmonitor-linux\nseries, I think.\n\nCiao,\nJohannes\n"},{"id":"540940","messageId":"20260405051528.74435-1-github@paulisageek.com","threadId":"64696","inReplyTo":"b96ed977-525e-c3fc-a626-db1a4b3da376@gmx.de","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-04-05T05:15:28Z","receivedAt":"2026-04-05T05:15:31Z","isPatch":true,"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> So the actual bug fix would be to ensure that `write_shared_index()`\n> produces a hash even if `index.skipHash=true`, because that hash is\n> needed to identify the shared index.\n\nYou're right, and I verified this. I reverted my fsmonitor.c patch\nfrom seen, applied the fix below in write_shared_index(), and ran\nt9210 with GIT_TEST_SPLIT_INDEX=yes on Fedora:\n\n  Without either fix:     not ok 12, not ok 13 (BUG assertion)\n  With only skipHash fix: passed all 22 test(s)\n\nThe fsmonitor.c patch is not needed. I'll drop it from the next\nversion of the series.\n\nHappy to submit this as a separate patch or include it in my\nseries if that's helpful. Or if someone is already working on the\nskipHash + split-index interaction, I'll stay out of the way.\n\n--- >8 ---\n\ndiff --git a/read-cache.c b/read-cache.c\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ write_shared_index\n \tmove_cache_to_base_index(istate);\n \tconvert_to_sparse(istate, 0);\n\n-\ttrace2_region_enter_printf(\"index\", \"shared/do_write_index\",\n-\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n-\tret = do_write_index(si->base, *temp, WRITE_NO_EXTENSION, flags);\n-\ttrace2_region_leave_printf(\"index\", \"shared/do_write_index\",\n-\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n+\t/*\n+\t * The shared index is identified by the hash of its contents\n+\t * (sharedindex.<oid>).  If index.skipHash is set, do_write_index()\n+\t * would produce an all-zero hash and the shared index would not\n+\t * be found on re-read (is_null_oid() check in read_index_from()).\n+\t * Temporarily force hashing for the shared index write.\n+\t */\n+\t{\n+\t\tstruct repository *r = the_repository;\n+\t\tint save_skip_hash;\n+\n+\t\tprepare_repo_settings(r);\n+\t\tsave_skip_hash = r->settings.index_skip_hash;\n+\t\tr->settings.index_skip_hash = 0;\n+\n+\t\ttrace2_region_enter_printf(\"index\", \"shared/do_write_index\",\n+\t\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n+\t\tret = do_write_index(si->base, *temp, WRITE_NO_EXTENSION, flags);\n+\t\ttrace2_region_leave_printf(\"index\", \"shared/do_write_index\",\n+\t\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n+\n+\t\tr->settings.index_skip_hash = save_skip_hash;\n+\t}\n\n \tif (was_full)\n \t\tensure_full_index(istate);\n\nPaul\n"},{"id":"540946","messageId":"cd82f960-88ff-661e-1e31-a119beb817e7@gmx.de","threadId":"64696","inReplyTo":"20260405051528.74435-1-github@paulisageek.com","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2026-04-05T09:26:02Z","receivedAt":"2026-04-05T09:26:12Z","isPatch":true,"body":"Hi Paul,\n\nOn Sat, 4 Apr 2026, Paul Tarjan wrote:\n\n> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n> \n> > So the actual bug fix would be to ensure that `write_shared_index()`\n> > produces a hash even if `index.skipHash=true`, because that hash is\n> > needed to identify the shared index.\n> \n> You're right, and I verified this. I reverted my fsmonitor.c patch\n> from seen, applied the fix below in write_shared_index(), and ran\n> t9210 with GIT_TEST_SPLIT_INDEX=yes on Fedora:\n> \n>   Without either fix:     not ok 12, not ok 13 (BUG assertion)\n>   With only skipHash fix: passed all 22 test(s)\n> \n> The fsmonitor.c patch is not needed. I'll drop it from the next\n> version of the series.\n\nThank you for confirming.\n\n> Happy to submit this as a separate patch or include it in my\n> series if that's helpful. Or if someone is already working on the\n> skipHash + split-index interaction, I'll stay out of the way.\n> \n> --- >8 ---\n> \n> diff --git a/read-cache.c b/read-cache.c\n> --- a/read-cache.c\n> +++ b/read-cache.c\n> @@ write_shared_index\n>  \tmove_cache_to_base_index(istate);\n>  \tconvert_to_sparse(istate, 0);\n> \n> -\ttrace2_region_enter_printf(\"index\", \"shared/do_write_index\",\n> -\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n> -\tret = do_write_index(si->base, *temp, WRITE_NO_EXTENSION, flags);\n> -\ttrace2_region_leave_printf(\"index\", \"shared/do_write_index\",\n> -\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n> +\t/*\n> +\t * The shared index is identified by the hash of its contents\n> +\t * (sharedindex.<oid>).  If index.skipHash is set, do_write_index()\n> +\t * would produce an all-zero hash and the shared index would not\n> +\t * be found on re-read (is_null_oid() check in read_index_from()).\n> +\t * Temporarily force hashing for the shared index write.\n> +\t */\n> +\t{\n> +\t\tstruct repository *r = the_repository;\n> +\t\tint save_skip_hash;\n> +\n> +\t\tprepare_repo_settings(r);\n> +\t\tsave_skip_hash = r->settings.index_skip_hash;\n> +\t\tr->settings.index_skip_hash = 0;\n> +\n> +\t\ttrace2_region_enter_printf(\"index\", \"shared/do_write_index\",\n> +\t\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n> +\t\tret = do_write_index(si->base, *temp, WRITE_NO_EXTENSION, flags);\n> +\t\ttrace2_region_leave_printf(\"index\", \"shared/do_write_index\",\n> +\t\t\t\t\t   the_repository, \"%s\", get_tempfile_path(*temp));\n> +\n> +\t\tr->settings.index_skip_hash = save_skip_hash;\n> +\t}\n> \n>  \tif (was_full)\n>  \t\tensure_full_index(istate);\n\nThis patch essentially tells Git to disobey the `index.skipHash` config,\nat least under some circumstances. While that _looks_ like it works around\nthe observed problem, it is unlikely to be desirable in the long run\nbecause such an inconsistency is prone to cause problems down the line.\n\nThe fundamental problem at hand is that the split-index _requires_ the\nindex' hash to be calculated, while the `index.skipHash` config\nspecifically _skips_ it. In other words, those two features are\nfundamentally incompatible with one another.\n\nMind you, I could imagine a sort of hacky way to make them work with each\nother: rely on the fact that padding the mtime's raw ytes with enough NULs\nto fill the OID array would result in a sort of fake OID that is as\nunlikely to clash with a real SHA as any other real SHA. In other words,\nunder `index.skipHash`, instead of accepting a `base_oid` that consists of\nall NULs, fake one that won't fall into the `is_null_oid()` trap.\n\nThis approach is still fraught with challenges. For one, a written\n`.git/index` file will not indicate whether it was written with or without\n`index.skipHash` (unlike the split-index, which results in a `link`\nextension to be included), unless you count that all-NUL OID as a hint.\nTherefore, just changing the OID that is written under `index.skipHash`\nwon't work, you'd have to introduce some sort of flag into the index file\nformat _and_ then you'd run into compatibility issues with other Git\nimplementations (or older Git versions) trying to read the index file\nwritten with that flag.\n\nSo the safest approach I can think of really is what I suggested, to force\nthe `GIT_TEST_SPLIT_INDEX` variable to be unset in `t9210-scalar.sh`.\n\nIt would probably also make sense to contribute a separate patch that lets\nGit error out if it is asked to read or write a split-index under\n`index.skipHash`, but that might very well lead down another rabbit hole,\ntherefore I won't recommend it.\n\nCiao,\nJohannes\n\nI briefly considered skipping t9210 altogether if that variable is set,\nbut then all of those tests would be skipped in `linux-TEST-vars`, which\nwould be counter-productive.\n\nCiao,\nJohannes\n"},{"id":"540953","messageId":"20260405172542.94160-1-github@paulisageek.com","threadId":"64696","inReplyTo":"cd82f960-88ff-661e-1e31-a119beb817e7@gmx.de","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-04-05T17:25:42Z","receivedAt":"2026-04-05T17:25:45Z","isPatch":true,"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> So the safest approach I can think of really is what I suggested,\n> to force the `GIT_TEST_SPLIT_INDEX` variable to be unset in\n> `t9210-scalar.sh`.\n\nThanks for the thorough analysis. I've dropped both the\nfsmonitor.c workaround and the read-cache.c skipHash fix, and\nadded sane_unset GIT_TEST_SPLIT_INDEX to the scalar clone tests\nthat hit this (tests 12, 13, and 22), matching the existing\nworkaround in test 16.\n\nVerified on Fedora: seen + fsmonitor-linux with\nGIT_TEST_SPLIT_INDEX=yes passes all 22 tests.\n\n--- >8 ---\n\ndiff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh\n--- a/t/t9210-scalar.sh\n+++ b/t/t9210-scalar.sh\n@@ -152,6 +152,10 @@ test_expect_success 'set up repository to clone' '\n '\n\n test_expect_success 'scalar clone' '\n+\t# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n+\t# incompatible: the shared index gets a null OID and fails to\n+\t# load on re-read.\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tsecond=$(git rev-parse --verify second:second.t) &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \t(\n@@ -182,6 +186,7 @@ test_expect_success 'scalar clone' '\n '\n\n test_expect_success 'scalar clone --no-... opts' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \t# Note: redirect stderr always to avoid having a verbose test\n \t# run result in a difference in the --[no-]progress option.\n \tGIT_TRACE2_EVENT=\"$(pwd)/no-opt-trace\" scalar clone \\\n@@ -307,6 +312,7 @@ test_expect_success '`scalar [...] <dir>` errors out when dir is missing' '\n\n SQ=\"'\"\n test_expect_success UNZIP 'scalar diagnose' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \tgit repack &&\n\nPaul\n"},{"id":"540983","messageId":"xmqq3418c9qq.fsf@gitster.g","threadId":"64696","inReplyTo":"cd82f960-88ff-661e-1e31-a119beb817e7@gmx.de","subject":"Re: [PATCH v12 13/13] fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-06T16:51:57Z","receivedAt":"2026-04-06T16:52:00Z","isPatch":true,"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> This patch essentially tells Git to disobey the `index.skipHash` config,\n> at least under some circumstances. While that _looks_ like it works around\n> the observed problem, it is unlikely to be desirable in the long run\n> because such an inconsistency is prone to cause problems down the line.\n>\n> The fundamental problem at hand is that the split-index _requires_ the\n> index' hash to be calculated, while the `index.skipHash` config\n> specifically _skips_ it. In other words, those two features are\n> fundamentally incompatible with one another.\n\nThanks for clearly spelling out the core problem ...\n\n> So the safest approach I can think of really is what I suggested, to force\n> the `GIT_TEST_SPLIT_INDEX` variable to be unset in `t9210-scalar.sh`.\n\n... and a clear recommendation.  Very much appreciated.\n\n"},{"id":"540991","messageId":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v12.git.git.1774937958.gitgitgadget@gmail.com","subject":"[PATCH v13 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:45Z","receivedAt":"2026-04-06T17:55:01Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v12:\n\n * Dropped both the fsmonitor.c workaround and the read-cache.c skipHash fix\n   per Dscho's review: split-index and index.skipHash are fundamentally\n   incompatible\n * Added sane_unset GIT_TEST_SPLIT_INDEX to scalar clone tests in t9210\n   (patch 1/13), matching the existing workaround in test 16\n\nChanges since v11:\n\n * Fix t9210 BUG assertion with GIT_TEST_SPLIT_INDEX=yes: guard\n   fsmonitor_dirty bitmap access against split-index having fewer entries\n   than the bitmap expects\n\nChanges since v10:\n\n * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n   per Junio's clarification (same as v8)\n\nChanges since v9:\n\n * Fixed Windows build: close_fd_above_stderr() compiles as a no-op on\n   Windows since there is no fork/exec\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review, with close_fd_above_stderr() as\n   a ready-made callback\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (13):\n  t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  12 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n t/t9210-scalar.sh                             |   6 +\n 19 files changed, 1268 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v13\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v13\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v12:\n\n  -:  ---------- >  1:  28c5aca413 t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n  1:  4d4dec8fa1 =  2:  9f666beea7 fsmonitor: fix khash memory leak in do_handle_client\n  2:  cb270120f0 =  3:  a4a65a6dfa fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  3:  44a063074d =  4:  427073fc38 compat/win32: add pthread_cond_timedwait\n  4:  b1081d1e13 =  5:  71effc4d47 fsmonitor: use pthread_cond_timedwait for cookie wait\n  5:  dec0fb144f =  6:  e897193b0e fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  6:  b2aaadb4ae =  7:  e13d938ddb fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  7:  03cf12d01b =  8:  3431eae60f fsmonitor: implement filesystem change listener for Linux\n  8:  50f5b4676e =  9:  7ce0ab87fb run-command: add close_fd_above_stderr option\n  9:  057b3098bc = 10:  2bf134a041 fsmonitor: close inherited file descriptors and detach in daemon\n 10:  e6bc3bfcb2 = 11:  cb4d511f21 fsmonitor: add timeout to daemon stop command\n 11:  81f8cd1599 = 12:  9a8647884e fsmonitor: add tests for Linux\n 12:  8fa6a74e0d = 13:  a5fc9ad415 fsmonitor: convert shown khash to strset in do_handle_client\n 13:  84ddbb30bb <  -:  ---------- fsmonitor: fix split-index bitmap bounds in tweak_fsmonitor()\n\n-- \ngitgitgadget\n"},{"id":"540992","messageId":"28c5aca413dc0966df62a3d04f8ed76bdd9a5bf1.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 01/13] t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:46Z","receivedAt":"2026-04-06T17:55:02Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nindex.skipHash (Scalar default) and split-index are incompatible:\nthe shared index gets a null OID when skipHash skips computing the\nhash, and the null OID causes the shared index to not be loaded on\nre-read.  This triggers a BUG assertion in fsmonitor when the\nfsmonitor_dirty bitmap references more entries than the (now empty)\nindex has.\n\nDisable GIT_TEST_SPLIT_INDEX in the scalar clone tests that hit\nthis, matching the existing workaround in test 16.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t9210-scalar.sh | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh\nindex 009437a5f3..f2a6df77ce 100755\n--- a/t/t9210-scalar.sh\n+++ b/t/t9210-scalar.sh\n@@ -152,6 +152,10 @@ test_expect_success 'set up repository to clone' '\n '\n \n test_expect_success 'scalar clone' '\n+\t# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n+\t# incompatible: the shared index gets a null OID and fails to\n+\t# load on re-read.\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tsecond=$(git rev-parse --verify second:second.t) &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \t(\n@@ -182,6 +186,7 @@ test_expect_success 'scalar clone' '\n '\n \n test_expect_success 'scalar clone --no-... opts' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \t# Note: redirect stderr always to avoid having a verbose test\n \t# run result in a difference in the --[no-]progress option.\n \tGIT_TRACE2_EVENT=\"$(pwd)/no-opt-trace\" scalar clone \\\n@@ -307,6 +312,7 @@ test_expect_success '`scalar [...] <dir>` errors out when dir is missing' '\n \n SQ=\"'\"\n test_expect_success UNZIP 'scalar diagnose' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \tgit repack &&\n \techo \"$(pwd)/.git/objects/\" >>cloned/src/.git/objects/info/alternates &&\n-- \ngitgitgadget\n\n"},{"id":"540993","messageId":"9f666beea7aa9a5f38e91ccdf4c2806506acadf4.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 02/13] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:47Z","receivedAt":"2026-04-06T17:55:03Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"540994","messageId":"a4a65a6dfaf702af7db34fda6012a5fe024cb1b6.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 03/13] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:48Z","receivedAt":"2026-04-06T17:55:05Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"540995","messageId":"427073fc38e6f1e41d4db844758978ce40d061db.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 04/13] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:49Z","receivedAt":"2026-04-06T17:55:06Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"540996","messageId":"71effc4d47d56a2ec7909f589be4776882dd703e.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 05/13] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:50Z","receivedAt":"2026-04-06T17:55:08Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"540997","messageId":"e897193b0e62ff9085e5c9798545677c18f9f862.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 06/13] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:51Z","receivedAt":"2026-04-06T17:55:09Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"540998","messageId":"e13d938ddbd42d4f3fd765655250b47e96f20749.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 07/13] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:52Z","receivedAt":"2026-04-06T17:55:10Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"540999","messageId":"3431eae60f3344e478177ad424a34ce36a02419d.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 08/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:53Z","receivedAt":"2026-04-06T17:55:13Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"541000","messageId":"7ce0ab87fbdb80e6a781ebc3fc3070af793682c6.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 09/13] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:54Z","receivedAt":"2026-04-06T17:55:14Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a close_fd_above_stderr flag to struct child_process.  When set,\nthe child closes file descriptors 3 and above between fork and exec\n(skipping the child-notifier pipe), capped at sysconf(_SC_OPEN_MAX)\nor 4096, whichever is smaller.  This prevents the child from\ninheriting pipe endpoints or other descriptors from the parent\nenvironment (e.g., the test harness).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 12 ++++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..f4361906c9 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -546,6 +546,7 @@ static void atfork_parent(struct atfork_state *as)\n \t\t\"restoring signal mask\");\n #endif\n }\n+\n #endif /* GIT_WINDOWS_NATIVE */\n \n static inline void set_cloexec(int fd)\n@@ -832,6 +833,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..fdaa01e140 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the child from inheriting\n+\t * pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"541001","messageId":"2bf134a041159dbbb4982a31ddf242b5331ad31c.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 10/13] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:55Z","receivedAt":"2026-04-06T17:55:16Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..b2a816dc3f 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"541002","messageId":"cb4d511f21ef05941ce7a54ee45b88c72f6497a2.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 11/13] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:56Z","receivedAt":"2026-04-06T17:55:17Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex b2a816dc3f..53d8ad1f0d 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"541003","messageId":"9a8647884e4106b13da8e9b628969244a57370d4.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 12/13] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:57Z","receivedAt":"2026-04-06T17:55:18Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"541004","messageId":"a5fc9ad4156a6f6ccf954e9b2571774b2bbd2036.1775498098.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v13 13/13] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-06T17:54:58Z","receivedAt":"2026-04-06T17:55:20Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 53d8ad1f0d..f920cf3a82 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"541084","messageId":"xmqqika24mj0.fsf@gitster.g","threadId":"64696","inReplyTo":"28c5aca413dc0966df62a3d04f8ed76bdd9a5bf1.1775498098.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v13 01/13] t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-07T19:07:31Z","receivedAt":"2026-04-07T19:07:35Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Paul Tarjan <github@paulisageek.com>\n>\n> index.skipHash (Scalar default) and split-index are incompatible:\n> the shared index gets a null OID when skipHash skips computing the\n> hash, and the null OID causes the shared index to not be loaded on\n> re-read.  This triggers a BUG assertion in fsmonitor when the\n> fsmonitor_dirty bitmap references more entries than the (now empty)\n> index has.\n>\n> Disable GIT_TEST_SPLIT_INDEX in the scalar clone tests that hit\n> this, matching the existing workaround in test 16.\n>\n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  t/t9210-scalar.sh | 6 ++++++\n>  1 file changed, 6 insertions(+)\n\nThe same treatment is needed for t9211, I suspect.\n\n--- >8 ---\n#!/bin/sh\n\nexport OPENSSL_SHA1_UNSAFE=YesPlease\nexport GIT_TEST_SPLIT_INDEX=yes\nexport GIT_TEST_FULL_IN_PACK_ARRAY=true\nexport GIT_TEST_OE_SIZE=10\nexport GIT_TEST_OE_DELTA_SIZE=5\nexport GIT_TEST_COMMIT_GRAPH=1\nexport GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1\nexport GIT_TEST_MULTI_PACK_INDEX=1\nexport GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=1\nexport GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master\nexport GIT_TEST_NO_WRITE_REV_INDEX=1\nexport GIT_TEST_CHECKOUT_WORKERS=2\nexport GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL=1\n\nmake && cd t && sh t9211-*.sh\n--- 8< ---\n"},{"id":"541085","messageId":"xmqqeckq4mfo.fsf@gitster.g","threadId":"64696","inReplyTo":"xmqqika24mj0.fsf@gitster.g","subject":"Re: [PATCH v13 01/13] t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-07T19:09:31Z","receivedAt":"2026-04-07T19:09:33Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> \"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Paul Tarjan <github@paulisageek.com>\n>>\n>> index.skipHash (Scalar default) and split-index are incompatible:\n>> the shared index gets a null OID when skipHash skips computing the\n>> hash, and the null OID causes the shared index to not be loaded on\n>> re-read.  This triggers a BUG assertion in fsmonitor when the\n>> fsmonitor_dirty bitmap references more entries than the (now empty)\n>> index has.\n>>\n>> Disable GIT_TEST_SPLIT_INDEX in the scalar clone tests that hit\n>> this, matching the existing workaround in test 16.\n>>\n>> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n>> ---\n>>  t/t9210-scalar.sh | 6 ++++++\n>>  1 file changed, 6 insertions(+)\n>\n> The same treatment is needed for t9211, I suspect.\n\nNot just \"suspect\", but it does break exactly at t9211.\n\n  https://github.com/git/git/actions/runs/24060447371/job/70175464363\n\n>\n> --- >8 ---\n> #!/bin/sh\n>\n> export OPENSSL_SHA1_UNSAFE=YesPlease\n> export GIT_TEST_SPLIT_INDEX=yes\n> export GIT_TEST_FULL_IN_PACK_ARRAY=true\n> export GIT_TEST_OE_SIZE=10\n> export GIT_TEST_OE_DELTA_SIZE=5\n> export GIT_TEST_COMMIT_GRAPH=1\n> export GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1\n> export GIT_TEST_MULTI_PACK_INDEX=1\n> export GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=1\n> export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master\n> export GIT_TEST_NO_WRITE_REV_INDEX=1\n> export GIT_TEST_CHECKOUT_WORKERS=2\n> export GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL=1\n>\n> make && cd t && sh t9211-*.sh\n> --- 8< ---\n"},{"id":"541115","messageId":"20260408042625.31843-1-github@paulisageek.com","threadId":"64696","inReplyTo":"xmqqse9g0xbx.fsf@gitster.g","subject":"Re: [PATCH v13 01/13] t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests","fromName":"Paul Tarjan","fromEmail":"paul@paultarjan.com","sentAt":"2026-04-08T04:26:25Z","receivedAt":"2026-04-08T04:26:30Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Not just \"suspect\", but it does break exactly at t9211.\n\nThanks. I've added sane_unset GIT_TEST_SPLIT_INDEX at the top of\nt9211 (every test there does scalar clone) and verified both t9210\nand t9211 pass with GIT_TEST_SPLIT_INDEX=yes on Fedora against\nseen.\n\nPaul\n"},{"id":"541197","messageId":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v13.git.git.1775498098.gitgitgadget@gmail.com","subject":"[PATCH v14 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:22Z","receivedAt":"2026-04-09T04:59:38Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v13:\n\n * Also disable GIT_TEST_SPLIT_INDEX in t9211 per Junio's feedback (every\n   test does scalar clone)\n\nChanges since v12:\n\n * Dropped both the fsmonitor.c workaround and the read-cache.c skipHash fix\n   per Dscho's review: split-index and index.skipHash are fundamentally\n   incompatible\n * Added sane_unset GIT_TEST_SPLIT_INDEX to scalar clone tests in t9210\n   (patch 1/13)\n\nChanges since v11:\n\n * Fix t9210 BUG assertion with GIT_TEST_SPLIT_INDEX=yes: guard\n   fsmonitor_dirty bitmap access against split-index having fewer entries\n   than the bitmap expects\n\nChanges since v10:\n\n * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n   per Junio's clarification (same as v8)\n\nChanges since v9:\n\n * Fixed Windows build: close_fd_above_stderr() compiles as a no-op on\n   Windows since there is no fork/exec\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review, with close_fd_above_stderr() as\n   a ready-made callback\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (13):\n  t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  12 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  |  89 ++-\n t/t9210-scalar.sh                             |   6 +\n t/t9211-scalar-clone.sh                       |   5 +\n 20 files changed, 1273 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v14\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v14\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v13:\n\n  1:  28c5aca413 !  1:  721a951423 t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n     @@ Metadata\n      Author: Paul Tarjan <github@paulisageek.com>\n      \n       ## Commit message ##\n     -    t9210: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n     +    t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n      \n          index.skipHash (Scalar default) and split-index are incompatible:\n          the shared index gets a null OID when skipHash skips computing the\n     @@ Commit message\n          index has.\n      \n          Disable GIT_TEST_SPLIT_INDEX in the scalar clone tests that hit\n     -    this, matching the existing workaround in test 16.\n     +    this: tests 12, 13, and 22 in t9210 (matching the existing\n     +    workaround in test 16), and all of t9211 (every test does scalar\n     +    clone).\n      \n          Signed-off-by: Paul Tarjan <github@paulisageek.com>\n      \n     @@ t/t9210-scalar.sh: test_expect_success '`scalar [...] <dir>` errors out when dir\n       \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n       \tgit repack &&\n       \techo \"$(pwd)/.git/objects/\" >>cloned/src/.git/objects/info/alternates &&\n     +\n     + ## t/t9211-scalar-clone.sh ##\n     +@@ t/t9211-scalar-clone.sh: test_description='test the `scalar clone` subcommand'\n     + GIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt,launchctl:true,schtasks:true\"\n     + export GIT_TEST_MAINT_SCHEDULER\n     + \n     ++# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n     ++# incompatible: the shared index gets a null OID and fails to\n     ++# load on re-read.  Every test here uses scalar clone.\n     ++sane_unset GIT_TEST_SPLIT_INDEX\n     ++\n     + test_expect_success 'set up repository to clone' '\n     + \trm -rf .git &&\n     + \tgit init to-clone &&\n  2:  9f666beea7 =  2:  1283d25968 fsmonitor: fix khash memory leak in do_handle_client\n  3:  a4a65a6dfa =  3:  11ba6ca9ac fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  4:  427073fc38 =  4:  a0d430c2f4 compat/win32: add pthread_cond_timedwait\n  5:  71effc4d47 =  5:  7b62c95c44 fsmonitor: use pthread_cond_timedwait for cookie wait\n  6:  e897193b0e =  6:  7086cd4530 fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  7:  e13d938ddb =  7:  46e8c2b74f fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  8:  3431eae60f =  8:  b3f40a497b fsmonitor: implement filesystem change listener for Linux\n  9:  7ce0ab87fb =  9:  5791edbef2 run-command: add close_fd_above_stderr option\n 10:  2bf134a041 = 10:  22d425ebeb fsmonitor: close inherited file descriptors and detach in daemon\n 11:  cb4d511f21 = 11:  fd6bdc8c55 fsmonitor: add timeout to daemon stop command\n 12:  9a8647884e = 12:  f85983ca93 fsmonitor: add tests for Linux\n 13:  a5fc9ad415 = 13:  2085b21e23 fsmonitor: convert shown khash to strset in do_handle_client\n\n-- \ngitgitgadget\n"},{"id":"541198","messageId":"721a95142337db84209fdfe76b2fe34c98ed34ee.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 01/13] t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:23Z","receivedAt":"2026-04-09T04:59:38Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nindex.skipHash (Scalar default) and split-index are incompatible:\nthe shared index gets a null OID when skipHash skips computing the\nhash, and the null OID causes the shared index to not be loaded on\nre-read.  This triggers a BUG assertion in fsmonitor when the\nfsmonitor_dirty bitmap references more entries than the (now empty)\nindex has.\n\nDisable GIT_TEST_SPLIT_INDEX in the scalar clone tests that hit\nthis: tests 12, 13, and 22 in t9210 (matching the existing\nworkaround in test 16), and all of t9211 (every test does scalar\nclone).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t9210-scalar.sh       | 6 ++++++\n t/t9211-scalar-clone.sh | 5 +++++\n 2 files changed, 11 insertions(+)\n\ndiff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh\nindex 009437a5f3..f2a6df77ce 100755\n--- a/t/t9210-scalar.sh\n+++ b/t/t9210-scalar.sh\n@@ -152,6 +152,10 @@ test_expect_success 'set up repository to clone' '\n '\n \n test_expect_success 'scalar clone' '\n+\t# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n+\t# incompatible: the shared index gets a null OID and fails to\n+\t# load on re-read.\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tsecond=$(git rev-parse --verify second:second.t) &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \t(\n@@ -182,6 +186,7 @@ test_expect_success 'scalar clone' '\n '\n \n test_expect_success 'scalar clone --no-... opts' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \t# Note: redirect stderr always to avoid having a verbose test\n \t# run result in a difference in the --[no-]progress option.\n \tGIT_TRACE2_EVENT=\"$(pwd)/no-opt-trace\" scalar clone \\\n@@ -307,6 +312,7 @@ test_expect_success '`scalar [...] <dir>` errors out when dir is missing' '\n \n SQ=\"'\"\n test_expect_success UNZIP 'scalar diagnose' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \tgit repack &&\n \techo \"$(pwd)/.git/objects/\" >>cloned/src/.git/objects/info/alternates &&\ndiff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh\nindex bfbf22a462..2043f48a1a 100755\n--- a/t/t9211-scalar-clone.sh\n+++ b/t/t9211-scalar-clone.sh\n@@ -8,6 +8,11 @@ test_description='test the `scalar clone` subcommand'\n GIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt,launchctl:true,schtasks:true\"\n export GIT_TEST_MAINT_SCHEDULER\n \n+# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n+# incompatible: the shared index gets a null OID and fails to\n+# load on re-read.  Every test here uses scalar clone.\n+sane_unset GIT_TEST_SPLIT_INDEX\n+\n test_expect_success 'set up repository to clone' '\n \trm -rf .git &&\n \tgit init to-clone &&\n-- \ngitgitgadget\n\n"},{"id":"541199","messageId":"1283d259684c0ce1484d533d78076d059106d84d.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 02/13] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:24Z","receivedAt":"2026-04-09T04:59:40Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"541200","messageId":"11ba6ca9aca3a693ce3eb03df802e2c8beaa2019.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 03/13] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:25Z","receivedAt":"2026-04-09T04:59:41Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"541201","messageId":"a0d430c2f46b598f21943cd0ba846f29d6b3c3d6.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 04/13] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:26Z","receivedAt":"2026-04-09T04:59:43Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"541202","messageId":"7b62c95c44bf1981d576e19aca77d26ce77f57b5.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 05/13] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:27Z","receivedAt":"2026-04-09T04:59:44Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"541203","messageId":"7086cd4530c87978661890d130246ca8fc964c35.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 06/13] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:28Z","receivedAt":"2026-04-09T04:59:46Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"541204","messageId":"46e8c2b74f49d1624f4661f85bfbd9bc2454aa61.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 07/13] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:29Z","receivedAt":"2026-04-09T04:59:49Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"541206","messageId":"b3f40a497b9e28b5189eaa38c98d915376ce5b21.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 08/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:30Z","receivedAt":"2026-04-09T04:59:51Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"541205","messageId":"5791edbef245a00c158a8c865a0a9eb052151fbb.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 09/13] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:31Z","receivedAt":"2026-04-09T04:59:52Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a close_fd_above_stderr flag to struct child_process.  When set,\nthe child closes file descriptors 3 and above between fork and exec\n(skipping the child-notifier pipe), capped at sysconf(_SC_OPEN_MAX)\nor 4096, whichever is smaller.  This prevents the child from\ninheriting pipe endpoints or other descriptors from the parent\nenvironment (e.g., the test harness).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 12 ++++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..f4361906c9 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -546,6 +546,7 @@ static void atfork_parent(struct atfork_state *as)\n \t\t\"restoring signal mask\");\n #endif\n }\n+\n #endif /* GIT_WINDOWS_NATIVE */\n \n static inline void set_cloexec(int fd)\n@@ -832,6 +833,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..fdaa01e140 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the child from inheriting\n+\t * pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"541207","messageId":"22d425ebebf3a81b0e89caddf4fe1b4a94005c58.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 10/13] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:32Z","receivedAt":"2026-04-09T04:59:54Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..b2a816dc3f 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"541208","messageId":"fd6bdc8c55cc1acc07586815f09925e4ad6796c3.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 11/13] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:33Z","receivedAt":"2026-04-09T04:59:56Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex b2a816dc3f..53d8ad1f0d 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"541209","messageId":"f85983ca93761bf6cec115d680af8c7d2938505d.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 12/13] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:34Z","receivedAt":"2026-04-09T04:59:57Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n 1 file changed, 82 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..774da5ac60 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,58 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\ttest_might_fail maybe_timeout 30 \\\n+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n \trm -rf $1\n }\n \n@@ -67,7 +116,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +995,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1019,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"541210","messageId":"2085b21e23a4522e1848f6256a48817bd9d7ee09.1775710775.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v14 13/13] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-09T04:59:35Z","receivedAt":"2026-04-09T04:59:59Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 53d8ad1f0d..f920cf3a82 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"541282","messageId":"xmqqfr54t3h4.fsf@gitster.g","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"Re: [PATCH v14 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-09T18:04:23Z","receivedAt":"2026-04-09T18:04:26Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> This series builds on work from https://github.com/git/git/pull/1352 by Eric\n> DeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\n> updated to work with the current codebase and address all review feedback.\n>\n> Changes since v13:\n>\n>  * Also disable GIT_TEST_SPLIT_INDEX in t9211 per Junio's feedback (every\n>    test does scalar clone)\n\nThanks.  The range-diff since the last iteration looks good.\n\nWill queue.\n"},{"id":"541586","messageId":"ad6hovxCkwMTG11U@szeder.dev","threadId":"64696","inReplyTo":"f85983ca93761bf6cec115d680af8c7d2938505d.1775710775.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v14 12/13] fsmonitor: add tests for Linux","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-04-14T20:20:50Z","receivedAt":"2026-04-14T20:20:53Z","isPatch":true,"body":"On Thu, Apr 09, 2026 at 04:59:34AM +0000, Paul Tarjan via GitGitGadget wrote:\n> From: Paul Tarjan <github@paulisageek.com>\n> \n> Add a smoke test that verifies the filesystem actually delivers\n> inotify events to the daemon.  On some configurations (e.g.,\n> overlayfs with older kernels), inotify watches succeed but events\n> are never delivered.  The daemon cookie wait will time out, but\n> every subsequent test would fail.  Skip the entire test file early\n> when this is detected.\n> \n> Add a test that exercises rapid nested directory creation to verify\n> the daemon correctly handles the EEXIST race between recursive scan\n> and queued inotify events.  When IN_MASK_CREATE is available and a\n> directory watch is added during recursive registration, the kernel\n> may also deliver a queued IN_CREATE event for the same directory.\n> The second inotify_add_watch() returns EEXIST, which must be treated\n> as harmless.  An earlier version of the listener crashed in this\n> scenario.\n> \n> Reduce --start-timeout from the default 60 seconds to 10 seconds so\n> that tests fail promptly when the daemon cannot start.\n> \n> Harden the test helpers to work in environments without procps\n> (e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\n> process group ID when ps is unavailable, guard stop_git() against\n> an empty pgid, and redirect stderr from kill to /dev/null to avoid\n> noise when processes have already exited.\n> \n> Use set -m to enable job control in the submodule-pull test so that\n> the background git pull gets its own process group, preventing the\n> shell wait from blocking on the daemon.  setsid() in the previous\n> commit detaches the daemon itself, but the intermediate git pull\n> process still needs its own process group for the test shell to\n> manage it correctly.\n> \n> Signed-off-by: Paul Tarjan <github@paulisageek.com>\n> ---\n>  t/t7527-builtin-fsmonitor.sh | 89 +++++++++++++++++++++++++++++++++---\n>  1 file changed, 82 insertions(+), 7 deletions(-)\n> \n> diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\n> index 409cd0cd12..774da5ac60 100755\n> --- a/t/t7527-builtin-fsmonitor.sh\n> +++ b/t/t7527-builtin-fsmonitor.sh\n> @@ -10,9 +10,58 @@ then\n>  \ttest_done\n>  fi\n>  \n> +# Verify that the filesystem delivers events to the daemon.\n> +# On some configurations (e.g., overlayfs with older kernels),\n> +# inotify watches succeed but events are never delivered.  The\n> +# cookie wait will time out and the daemon logs a trace message.\n> +#\n> +# Use \"timeout\" (if available) to guard each step against hangs.\n> +maybe_timeout () {\n> +\tif type timeout >/dev/null 2>&1\n> +\tthen\n> +\t\ttimeout \"$@\"\n> +\telse\n> +\t\tshift\n> +\t\t\"$@\"\n> +\tfi\n> +}\n> +verify_fsmonitor_works () {\n> +\tgit init test_fsmonitor_smoke || return 1\n> +\n> +\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n> +\texport GIT_TRACE_FSMONITOR &&\n> +\tmaybe_timeout 30 \\\n> +\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n> +\t\t\t--start-timeout=10\n> +\tret=$?\n> +\tunset GIT_TRACE_FSMONITOR\n> +\tif test $ret -ne 0\n> +\tthen\n> +\t\trm -rf test_fsmonitor_smoke smoke.trace\n> +\t\treturn 1\n> +\tfi\n> +\n> +\tmaybe_timeout 10 \\\n> +\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n> +\t\t\t--token 0 >/dev/null 2>&1\n> +\tmaybe_timeout 5 \\\n> +\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n> +\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n> +\tret=$?\n> +\trm -rf test_fsmonitor_smoke smoke.trace\n> +\treturn $ret\n> +}\n> +\n> +if ! verify_fsmonitor_works\n> +then\n> +\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n> +\ttest_done\n> +fi\n> +\n>  stop_daemon_delete_repo () {\n>  \tr=$1 &&\n> -\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n> +\ttest_might_fail maybe_timeout 30 \\\n> +\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n\n\"test_might_fail\" only allows a few select commands and functions, and\nthe \"maybe_timeout\" helper function introduced in this patch is, of\ncourse, not one of them, so it returns with error and without running\nthe given command.  Consequently, after this test script is finished I\nhave still two fsmonitor daemon processes running in the background.\n\nAlas, this went unnoticed, because this patch broke the &&-chain and\nredirected \"test_might_fail\"'s\n\n  test_must_fail: only 'git' is allowed: maybe_timeout 30 git -C test_explicit fsmonitor--daemon stop\n\nerror messages to /dev/null.  With the &&-chain restored over 40 test\ncases fail because of this.\n\n>  \trm -rf $1\n>  }\n>  \n> @@ -67,7 +116,7 @@ start_daemon () {\n>  \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n>  \t\tfi &&\n>  \n> -\t\tgit $r fsmonitor--daemon start &&\n> +\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n>  \t\tgit $r fsmonitor--daemon status\n>  \t)\n>  }\n> @@ -520,6 +569,28 @@ test_expect_success 'directory changes to a file' '\n>  \tgrep \"^event: dir1$\" .git/trace\n>  '\n>  \n> +test_expect_success 'rapid nested directory creation' '\n> +\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n> +\n> +\tstart_daemon --tf \"$PWD/.git/trace\" &&\n> +\n> +\t# Rapidly create nested directories to exercise race conditions\n> +\t# where directory watches may be added concurrently during\n> +\t# event processing and recursive scanning.\n> +\tfor i in $(test_seq 1 20)\n> +\tdo\n> +\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n> +\tdone &&\n> +\n> +\t# Give the daemon time to process all events\n> +\tsleep 1 &&\n> +\n> +\ttest-tool fsmonitor-client query --token 0 &&\n> +\n> +\t# Verify daemon is still running (did not crash)\n> +\tgit fsmonitor--daemon status\n> +'\n> +\n>  # The next few test cases exercise the token-resync code.  When filesystem\n>  # drops events (because of filesystem velocity or because the daemon isn't\n>  # polling fast enough), we need to discard the cached data (relative to the\n> @@ -910,7 +981,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n>  start_git_in_background () {\n>  \tgit \"$@\" &\n>  \tgit_pid=$!\n> -\tgit_pgid=$(ps -o pgid= -p $git_pid)\n> +\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n> +\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n> +\tgit_pgid=\"${git_pgid## }\" &&\n> +\tgit_pgid=\"${git_pgid%% }\"\n>  \tnr_tries_left=10\n>  \twhile true\n>  \tdo\n> @@ -921,15 +995,16 @@ start_git_in_background () {\n>  \t\tfi\n>  \t\tsleep 1\n>  \t\tnr_tries_left=$(($nr_tries_left - 1))\n> -\tdone >/dev/null 2>&1 &\n> +\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n>  \twatchdog_pid=$!\n>  \twait $git_pid\n>  }\n>  \n>  stop_git () {\n> -\twhile kill -0 -- -$git_pgid\n> +\ttest -n \"$git_pgid\" || return 0\n> +\twhile kill -0 -- -$git_pgid 2>/dev/null\n>  \tdo\n> -\t\tkill -- -$git_pgid\n> +\t\tkill -- -$git_pgid 2>/dev/null\n>  \t\tsleep 1\n>  \tdone\n>  }\n> @@ -944,7 +1019,7 @@ stop_watchdog () {\n>  \n>  test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n>  \ttest_atexit \"stop_watchdog\" &&\n> -\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n> +\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n>  \n>  \tcreate_super super &&\n>  \tcreate_sub sub &&\n> -- \n> gitgitgadget\n> \n"},{"id":"541588","messageId":"xmqq5x5t9sxl.fsf@gitster.g","threadId":"64696","inReplyTo":"ad6hovxCkwMTG11U@szeder.dev","subject":"Re: [PATCH v14 12/13] fsmonitor: add tests for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-14T20:40:38Z","receivedAt":"2026-04-14T20:40:40Z","isPatch":true,"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n>>  stop_daemon_delete_repo () {\n>>  \tr=$1 &&\n>> -\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n>> +\ttest_might_fail maybe_timeout 30 \\\n>> +\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n>\n> \"test_might_fail\" only allows a few select commands and functions, and\n> the \"maybe_timeout\" helper function introduced in this patch is, of\n> course, not one of them, so it returns with error and without running\n> the given command.  Consequently, after this test script is finished I\n> have still two fsmonitor daemon processes running in the background.\n>\n> Alas, this went unnoticed, because this patch broke the &&-chain and\n> redirected \"test_might_fail\"'s\n>\n>   test_must_fail: only 'git' is allowed: maybe_timeout 30 git -C test_explicit fsmonitor--daemon stop\n>\n> error messages to /dev/null.  With the &&-chain restored over 40 test\n> cases fail because of this.\n>\n>>  \trm -rf $1\n>>  }\n\nYikes.  Would it help to apply a patch like this, then?\n\n t/t7527-builtin-fsmonitor.sh | 3 +--\n 1 file changed, 1 insertion(+), 2 deletions(-)\n\ndiff --git c/t/t7527-builtin-fsmonitor.sh w/t/t7527-builtin-fsmonitor.sh\nindex 774da5ac60..dfa06395f6 100755\n--- c/t/t7527-builtin-fsmonitor.sh\n+++ w/t/t7527-builtin-fsmonitor.sh\n@@ -60,8 +60,7 @@ fi\n \n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail maybe_timeout 30 \\\n-\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n+\tmaybe_timeout 30 git -C $r fsmonitor--daemon stop || : &&\n \trm -rf $1\n }\n \n"},{"id":"541599","messageId":"20260414221335.GA3483791@coredump.intra.peff.net","threadId":"64696","inReplyTo":"xmqq5x5t9sxl.fsf@gitster.g","subject":"Re: [PATCH v14 12/13] fsmonitor: add tests for Linux","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-14T22:13:35Z","receivedAt":"2026-04-14T22:13:36Z","isPatch":true,"body":"On Tue, Apr 14, 2026 at 01:40:38PM -0700, Junio C Hamano wrote:\n\n> diff --git c/t/t7527-builtin-fsmonitor.sh w/t/t7527-builtin-fsmonitor.sh\n> index 774da5ac60..dfa06395f6 100755\n> --- c/t/t7527-builtin-fsmonitor.sh\n> +++ w/t/t7527-builtin-fsmonitor.sh\n> @@ -60,8 +60,7 @@ fi\n>  \n>  stop_daemon_delete_repo () {\n>  \tr=$1 &&\n> -\ttest_might_fail maybe_timeout 30 \\\n> -\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n> +\tmaybe_timeout 30 git -C $r fsmonitor--daemon stop || : &&\n>  \trm -rf $1\n>  }\n\nDo we need to put it in curly braces to avoid interfering with the &&\nchain? Otherwise a failure of anything before the maybe_timeout will hit\nthe \"||\".\n\nI guess in this case it is just \"r=$1\", which will never fail, but it\nfeels like we should model best practice.\n\n-Peff\n"},{"id":"541642","messageId":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v14.git.git.1775710775.gitgitgadget@gmail.com","subject":"[PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:24Z","receivedAt":"2026-04-15T13:27:40Z","isPatch":true,"body":"This series implements the built-in fsmonitor daemon for Linux using the\ninotify API, bringing it to feature parity with the existing Windows and\nmacOS implementations. It also fixes two memory leaks in the\nplatform-independent daemon code and deduplicates the IPC and settings logic\nthat is now shared between macOS and Linux.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon. While inotify has the\nlimitation of requiring a separate watch on every directory (unlike macOS\nFSEvents, which can monitor an entire directory tree with a single watch),\nit operates without elevated privileges and provides the per-file event\ngranularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for\nevents with a 50-millisecond timeout, keeping the inotify queue well-drained\nto minimize the risk of overflows. Bidirectional hashmaps map between watch\ndescriptors and directory paths for efficient event resolution. Directory\nrenames are tracked using inotify cookie mechanism to correlate\nIN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale\nrenames where the matching IN_MOVED_TO never arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure all\nsubdirectories are monitored. The IN_MASK_CREATE flag is used where\navailable to prevent modifying existing watches, with a fallback for older\nkernels. When IN_MASK_CREATE is available and inotify_add_watch returns\nEEXIST, it means another thread or recursive scan has already registered the\nwatch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount\npoint information is read from /proc/mounts and matched against the statfs\nf_fsid to get accurate, human-readable filesystem type names for logging.\nWhen the .git directory is on a remote filesystem, the IPC socket falls back\nto $HOME or a user-configured directory via the fsmonitor.socketDir setting.\n\nThis series builds on work from https://github.com/git/git/pull/1352 by Eric\nDeCosta and https://github.com/git/git/pull/1667 by Marziyeh Esipreh,\nupdated to work with the current codebase and address all review feedback.\n\nChanges since v14:\n\n * Fix stop_daemon_delete_repo: test_might_fail doesn't allow maybe_timeout,\n   replaced with { ... || :; } per SZEDER/Peff review, restoring broken\n   &&-chain\n\nChanges since v13:\n\n * Also disable GIT_TEST_SPLIT_INDEX in t9211 per Junio's feedback (every\n   test does scalar clone)\n\nChanges since v12:\n\n * Dropped both the fsmonitor.c workaround and the read-cache.c skipHash fix\n   per Dscho's review: split-index and index.skipHash are fundamentally\n   incompatible\n * Added sane_unset GIT_TEST_SPLIT_INDEX to scalar clone tests in t9210\n   (patch 1/13)\n\nChanges since v11:\n\n * Fix t9210 BUG assertion with GIT_TEST_SPLIT_INDEX=yes: guard\n   fsmonitor_dirty bitmap access against split-index having fewer entries\n   than the bitmap expects\n\nChanges since v10:\n\n * Reverted pre_exec_cb callback back to simple close_fd_above_stderr flag\n   per Junio's clarification (same as v8)\n\nChanges since v9:\n\n * Fixed Windows build: close_fd_above_stderr() compiles as a no-op on\n   Windows since there is no fork/exec\n\nChanges since v8:\n\n * Replaced close_fd_above_stderr flag with generic pre_exec_cb callback in\n   struct child_process per Junio's review, with close_fd_above_stderr() as\n   a ready-made callback\n\nChanges since v7:\n\n * Added patch 12: convert khash to strset in do_handle_client (Patrick's\n   #leftoverbit suggestion)\n * Fixed \"Forcing shutdown\" trace message to start with lowercase\n * Fixed redundant statfs() call in find_mount() (caller already had the\n   result)\n * Fixed CMakeLists.txt GIT-BUILD-OPTIONS: was hardcoded to \"win32\" for\n   FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS, now uses the CMake\n   variables\n * Fixed uninitialized strset on trivial response path (STRSET_INIT)\n * Removed V9FS_MAGIC from get_fs_typename() to match is_remote_fs() (9p is\n   local VM mounts)\n * Split 30-second stop timeout into its own commit per review request\n * Fixed misleading indentation on shutdown assignment in handle_events()\n * Updated commit messages to describe all changes (test hardening,\n   fsmonitor-ipc.c spawn changes)\n * Updated Makefile comment for FSMONITOR_OS_SETTINGS to mention fsm-ipc\n\nChanges since v6:\n\n * Introduced FSMONITOR_OS_SETTINGS build variable (set to \"unix\" for macOS\n   and Linux, \"win32\" for Windows) to eliminate if/else conditionals in\n   Makefile, meson.build, and CMakeLists.txt per Junio's review\n * Moved fsm-path-utils from FSMONITOR_OS_SETTINGS to\n   FSMONITOR_DAEMON_BACKEND since path-utils files are platform-specific\n * Removed V9FS_MAGIC from remote filesystem detection (9p is used for local\n   VM/container host mounts where fsmonitor works fine)\n * Removed redundant #include <libgen.h> (already provided by\n   compat/posix.h)\n * Fixed cookie wait comment wording (\"see\" to \"observe\")\n * Rewrote commit messages for IPC and settings dedup patches\n\nChanges since v5:\n\n * Split monolithic commit into 10-patch series per Patrick's review\n * Deduplicated fsm-ipc and fsm-settings into shared Unix implementations\n * Rewrote commit message with prose paragraphs, explain inotify vs\n   fanotify, removed \"Issues addressed\" sections, added Based-on-patch-by\n   trailers\n * Removed redundant includes already provided by compat/posix.h\n * Fixed error/trace message capitalization per coding guidelines\n * Fixed stale rename check interval from 1000 seconds to 1 second\n * Changed poll timeout from 1ms to 50ms to reduce idle CPU wake-ups\n * Replaced infinite pthread_cond_wait cookie loop with one-second\n   pthread_cond_timedwait (prevents daemon hangs on overlay filesystems\n   where events are never delivered)\n * Added pthread_cond_timedwait to Windows pthread compatibility layer\n * Separated test into its own commit with smoke test that skips when\n   inotify events are not delivered (e.g., overlayfs with older kernels)\n * Fixed test hang on Fedora CI: stop_git() looped forever when ps was\n   unavailable because bash in POSIX/sh mode returns exit 0 from kill with\n   an empty process group argument. Fixed by falling back to /proc/$pid/stat\n   for process group ID and guarding stop_git against empty pgid.\n * Redirect spawn_daemon() stdout/stderr to /dev/null and close inherited\n   file descriptors to prevent the intermediate process from holding test\n   pipe file descriptors\n * Call setsid() on daemon detach to prevent shells with job control from\n   waiting on the daemon process group\n * Close inherited file descriptors 3-7 in the test watchdog subprocess\n * Added 30-second timeout to \"fsmonitor--daemon stop\" to prevent indefinite\n   blocking\n * Added helpful error message when inotify watch limit (max_user_watches)\n   is reached\n * Initialize fd_inotify to -1 and use fd >= 0 check for correct fd 0\n   handling\n * Use sysconf(_SC_OPEN_MAX) instead of hardcoded 1024 for fd close limit\n * Check setsid() return value\n\nChanges since v4:\n\n * Added Meson build support\n\nChanges since v3:\n\n * Fix crash on rapid nested directory creation (EEXIST from\n   inotify_add_watch with IN_MASK_CREATE)\n * Extensive stress testing\n\nChanges since v2:\n\n * Fix khash memory leak in do_handle_client\n\nChanges since v1:\n\n * Fix hashmap memory leak in fsmonitor_run_daemon()\n\nPaul Tarjan (13):\n  t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n  fsmonitor: fix khash memory leak in do_handle_client\n  fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  compat/win32: add pthread_cond_timedwait\n  fsmonitor: use pthread_cond_timedwait for cookie wait\n  fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  fsmonitor: implement filesystem change listener for Linux\n  run-command: add close_fd_above_stderr option\n  fsmonitor: close inherited file descriptors and detach in daemon\n  fsmonitor: add timeout to daemon stop command\n  fsmonitor: add tests for Linux\n  fsmonitor: convert shown khash to strset in do_handle_client\n\n Documentation/config/fsmonitor--daemon.adoc   |   4 +-\n Documentation/git-fsmonitor--daemon.adoc      |  28 +-\n Makefile                                      |   6 +-\n builtin/fsmonitor--daemon.c                   |  92 ++-\n compat/fsmonitor/fsm-health-linux.c           |  33 +\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |   0\n compat/fsmonitor/fsm-listen-linux.c           | 746 ++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c       | 217 +++++\n ...-settings-darwin.c => fsm-settings-unix.c} |   0\n compat/win32/pthread.c                        |  26 +\n compat/win32/pthread.h                        |   2 +\n config.mak.uname                              |  12 +-\n contrib/buildsystems/CMakeLists.txt           |  33 +-\n fsmonitor-ipc.c                               |   3 +\n meson.build                                   |  13 +-\n run-command.c                                 |  12 +\n run-command.h                                 |   9 +\n t/t7527-builtin-fsmonitor.sh                  |  88 ++-\n t/t9210-scalar.sh                             |   6 +\n t/t9211-scalar-clone.sh                       |   5 +\n 20 files changed, 1272 insertions(+), 63 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\n\nbase-commit: 3e0db84c88c57e70ac8be8c196dfa92c5d656fbc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2147%2Fptarjan%2Fclaude%2Fupdate-pr-1352-current-85Gk8-v15\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2147/ptarjan/claude/update-pr-1352-current-85Gk8-v15\nPull-Request: https://github.com/git/git/pull/2147\n\nRange-diff vs v14:\n\n  1:  721a951423 =  1:  721a951423 t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests\n  2:  1283d25968 =  2:  1283d25968 fsmonitor: fix khash memory leak in do_handle_client\n  3:  11ba6ca9ac =  3:  11ba6ca9ac fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon\n  4:  a0d430c2f4 =  4:  a0d430c2f4 compat/win32: add pthread_cond_timedwait\n  5:  7b62c95c44 =  5:  7b62c95c44 fsmonitor: use pthread_cond_timedwait for cookie wait\n  6:  7086cd4530 =  6:  7086cd4530 fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c\n  7:  46e8c2b74f =  7:  46e8c2b74f fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c\n  8:  b3f40a497b =  8:  b3f40a497b fsmonitor: implement filesystem change listener for Linux\n  9:  5791edbef2 =  9:  5791edbef2 run-command: add close_fd_above_stderr option\n 10:  22d425ebeb = 10:  22d425ebeb fsmonitor: close inherited file descriptors and detach in daemon\n 11:  fd6bdc8c55 = 11:  fd6bdc8c55 fsmonitor: add timeout to daemon stop command\n 12:  f85983ca93 ! 12:  ffffd64a4b fsmonitor: add tests for Linux\n     @@ t/t7527-builtin-fsmonitor.sh: then\n       stop_daemon_delete_repo () {\n       \tr=$1 &&\n      -\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n     -+\ttest_might_fail maybe_timeout 30 \\\n     -+\t\tgit -C $r fsmonitor--daemon stop 2>/dev/null\n     ++\t{ maybe_timeout 30 git -C $r fsmonitor--daemon stop 2>/dev/null || :; } &&\n       \trm -rf $1\n       }\n       \n 13:  2085b21e23 = 13:  ada3f1888b fsmonitor: convert shown khash to strset in do_handle_client\n\n-- \ngitgitgadget\n"},{"id":"541643","messageId":"721a95142337db84209fdfe76b2fe34c98ed34ee.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 01/13] t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:25Z","receivedAt":"2026-04-15T13:27:41Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nindex.skipHash (Scalar default) and split-index are incompatible:\nthe shared index gets a null OID when skipHash skips computing the\nhash, and the null OID causes the shared index to not be loaded on\nre-read.  This triggers a BUG assertion in fsmonitor when the\nfsmonitor_dirty bitmap references more entries than the (now empty)\nindex has.\n\nDisable GIT_TEST_SPLIT_INDEX in the scalar clone tests that hit\nthis: tests 12, 13, and 22 in t9210 (matching the existing\nworkaround in test 16), and all of t9211 (every test does scalar\nclone).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t9210-scalar.sh       | 6 ++++++\n t/t9211-scalar-clone.sh | 5 +++++\n 2 files changed, 11 insertions(+)\n\ndiff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh\nindex 009437a5f3..f2a6df77ce 100755\n--- a/t/t9210-scalar.sh\n+++ b/t/t9210-scalar.sh\n@@ -152,6 +152,10 @@ test_expect_success 'set up repository to clone' '\n '\n \n test_expect_success 'scalar clone' '\n+\t# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n+\t# incompatible: the shared index gets a null OID and fails to\n+\t# load on re-read.\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tsecond=$(git rev-parse --verify second:second.t) &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \t(\n@@ -182,6 +186,7 @@ test_expect_success 'scalar clone' '\n '\n \n test_expect_success 'scalar clone --no-... opts' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \t# Note: redirect stderr always to avoid having a verbose test\n \t# run result in a difference in the --[no-]progress option.\n \tGIT_TRACE2_EVENT=\"$(pwd)/no-opt-trace\" scalar clone \\\n@@ -307,6 +312,7 @@ test_expect_success '`scalar [...] <dir>` errors out when dir is missing' '\n \n SQ=\"'\"\n test_expect_success UNZIP 'scalar diagnose' '\n+\tsane_unset GIT_TEST_SPLIT_INDEX &&\n \tscalar clone \"file://$(pwd)\" cloned --single-branch &&\n \tgit repack &&\n \techo \"$(pwd)/.git/objects/\" >>cloned/src/.git/objects/info/alternates &&\ndiff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh\nindex bfbf22a462..2043f48a1a 100755\n--- a/t/t9211-scalar-clone.sh\n+++ b/t/t9211-scalar-clone.sh\n@@ -8,6 +8,11 @@ test_description='test the `scalar clone` subcommand'\n GIT_TEST_MAINT_SCHEDULER=\"crontab:test-tool crontab cron.txt,launchctl:true,schtasks:true\"\n export GIT_TEST_MAINT_SCHEDULER\n \n+# index.skipHash (Scalar default) and GIT_TEST_SPLIT_INDEX are\n+# incompatible: the shared index gets a null OID and fails to\n+# load on re-read.  Every test here uses scalar clone.\n+sane_unset GIT_TEST_SPLIT_INDEX\n+\n test_expect_success 'set up repository to clone' '\n \trm -rf .git &&\n \tgit init to-clone &&\n-- \ngitgitgadget\n\n"},{"id":"541644","messageId":"1283d259684c0ce1484d533d78076d059106d84d.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 02/13] fsmonitor: fix khash memory leak in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:26Z","receivedAt":"2026-04-15T13:27:43Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `shown` kh_str_t was freed with kh_release_str() at a point in\nthe code only reachable in the non-trivial response path.  When the\nclient receives a trivial response, the code jumps to the `cleanup`\nlabel, skipping the kh_release_str() call entirely and leaking the\nhash table.\n\nFix this by initializing `shown` to NULL and moving the cleanup to the\n`cleanup` label using kh_destroy_str(), which is safe to call on NULL.\nThis ensures the hash table is freed regardless of which code path is\ntaken.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 242c594646..bc4571938c 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -671,7 +671,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown;\n+\tkh_str_t *shown = NULL;\n \tint hash_ret;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n@@ -909,8 +909,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\ttotal_response_len += payload.len;\n \t}\n \n-\tkh_release_str(shown);\n-\n \tpthread_mutex_lock(&state->main_lock);\n \n \tif (token_data->client_ref_count > 0)\n@@ -954,6 +952,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n+\tkh_destroy_str(shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n\n"},{"id":"541645","messageId":"11ba6ca9aca3a693ce3eb03df802e2c8beaa2019.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 03/13] fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:27Z","receivedAt":"2026-04-15T13:27:44Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe `state.cookies` hashmap is initialized during daemon startup but\nnever freed during cleanup in the `done:` label of\nfsmonitor_run_daemon().  The cookie entries also have names allocated\nvia strbuf_detach() that must be freed individually.\n\nIterate the hashmap to free each cookie name, then call\nhashmap_clear_and_free() to release the entries and table.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex bc4571938c..d8d32b01ef 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1404,6 +1404,15 @@ static int fsmonitor_run_daemon(void)\n done:\n \tpthread_cond_destroy(&state.cookies_cond);\n \tpthread_mutex_destroy(&state.main_lock);\n+\t{\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct fsmonitor_cookie_item *cookie;\n+\n+\t\thashmap_for_each_entry(&state.cookies, &iter, cookie, entry)\n+\t\t\tfree(cookie->name);\n+\t\thashmap_clear_and_free(&state.cookies,\n+\t\t\t\t       struct fsmonitor_cookie_item, entry);\n+\t}\n \tfsm_listen__dtor(&state);\n \tfsm_health__dtor(&state);\n \n-- \ngitgitgadget\n\n"},{"id":"541646","messageId":"a0d430c2f46b598f21943cd0ba846f29d6b3c3d6.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 04/13] compat/win32: add pthread_cond_timedwait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:28Z","receivedAt":"2026-04-15T13:27:47Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a pthread_cond_timedwait() implementation to the Windows pthread\ncompatibility layer using SleepConditionVariableCS() with a millisecond\ntimeout computed from the absolute deadline.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/win32/pthread.c | 26 ++++++++++++++++++++++++++\n compat/win32/pthread.h |  2 ++\n 2 files changed, 28 insertions(+)\n\ndiff --git a/compat/win32/pthread.c b/compat/win32/pthread.c\nindex 7e93146963..398caa9602 100644\n--- a/compat/win32/pthread.c\n+++ b/compat/win32/pthread.c\n@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)\n \t\treturn err_win_to_posix(GetLastError());\n \treturn 0;\n }\n+\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime)\n+{\n+\tstruct timeval now;\n+\tlong long now_ms, deadline_ms;\n+\tDWORD timeout_ms;\n+\n+\tgettimeofday(&now, NULL);\n+\tnow_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;\n+\tdeadline_ms = (long long)abstime->tv_sec * 1000 +\n+\t\t      abstime->tv_nsec / 1000000;\n+\n+\tif (deadline_ms <= now_ms)\n+\t\treturn ETIMEDOUT;\n+\telse\n+\t\ttimeout_ms = (DWORD)(deadline_ms - now_ms);\n+\n+\tif (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {\n+\t\tDWORD err = GetLastError();\n+\t\tif (err == ERROR_TIMEOUT)\n+\t\t\treturn ETIMEDOUT;\n+\t\treturn err_win_to_posix(err);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/compat/win32/pthread.h b/compat/win32/pthread.h\nindex ccacc5a53b..d80df8d12a 100644\n--- a/compat/win32/pthread.h\n+++ b/compat/win32/pthread.h\n@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);\n pthread_t pthread_self(void);\n \n int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);\n+int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,\n+\t\t\t   const struct timespec *abstime);\n \n static inline void NORETURN pthread_exit(void *ret)\n {\n-- \ngitgitgadget\n\n"},{"id":"541647","messageId":"7b62c95c44bf1981d576e19aca77d26ce77f57b5.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 05/13] fsmonitor: use pthread_cond_timedwait for cookie wait","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:29Z","receivedAt":"2026-04-15T13:27:48Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe cookie wait in with_lock__wait_for_cookie() uses an infinite\npthread_cond_wait() loop.  The existing comment notes the desire\nto switch to pthread_cond_timedwait(), but the routine was not\navailable in git thread-utils.\n\nOn certain container or overlay filesystems, inotify watches may\nsucceed but events are never delivered.  In this case the daemon\nwould hang indefinitely waiting for the cookie event, which in\nturn causes the client to hang.\n\nReplace the infinite wait with a one-second timeout using\npthread_cond_timedwait().  If the timeout fires, report an\nerror and let the client proceed with a trivial (full-scan)\nresponse rather than blocking forever.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 37 ++++++++++++++++++++++++-------------\n 1 file changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex d8d32b01ef..c8ec7b722e 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -197,20 +197,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(\n \tunlink(cookie_pathname.buf);\n \n \t/*\n-\t * Technically, this is an infinite wait (well, unless another\n-\t * thread sends us an abort).  I'd like to change this to\n-\t * use `pthread_cond_timedwait()` and return an error/timeout\n-\t * and let the caller do the trivial response thing, but we\n-\t * don't have that routine in our thread-utils.\n-\t *\n-\t * After extensive beta testing I'm not really worried about\n-\t * this.  Also note that the above open() and unlink() calls\n-\t * will cause at least two FS events on that path, so the odds\n-\t * of getting stuck are pretty slim.\n+\t * Wait for the listener thread to observe the cookie file.\n+\t * Time out after a short interval so that the client\n+\t * does not hang forever if the filesystem does not deliver\n+\t * events (e.g., on certain container/overlay filesystems\n+\t * where inotify watches succeed but events never arrive).\n \t */\n-\twhile (cookie->result == FCIR_INIT)\n-\t\tpthread_cond_wait(&state->cookies_cond,\n-\t\t\t\t  &state->main_lock);\n+\t{\n+\t\tstruct timeval now;\n+\t\tstruct timespec ts;\n+\t\tint err = 0;\n+\n+\t\tgettimeofday(&now, NULL);\n+\t\tts.tv_sec = now.tv_sec + 1;\n+\t\tts.tv_nsec = now.tv_usec * 1000;\n+\n+\t\twhile (cookie->result == FCIR_INIT && !err)\n+\t\t\terr = pthread_cond_timedwait(&state->cookies_cond,\n+\t\t\t\t\t\t     &state->main_lock,\n+\t\t\t\t\t\t     &ts);\n+\t\tif (err == ETIMEDOUT && cookie->result == FCIR_INIT) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"cookie_wait timed out\");\n+\t\t\tcookie->result = FCIR_ERROR;\n+\t\t}\n+\t}\n \n done:\n \thashmap_remove(&state->cookies, &cookie->entry, NULL);\n-- \ngitgitgadget\n\n"},{"id":"541648","messageId":"7086cd4530c87978661890d130246ca8fc964c35.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 06/13] fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:30Z","receivedAt":"2026-04-15T13:27:51Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor IPC path logic in fsm-ipc-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-ipc-unix.c to reflect that it\nis shared by all Unix platforms.\n\nIntroduce FSMONITOR_OS_SETTINGS (set to \"unix\" for non-Windows, \"win32\"\nfor Windows) as a separate variable from FSMONITOR_DAEMON_BACKEND so\nthat the build files can distinguish between platform-specific files\n(listen, health, path-utils) and shared Unix files (ipc, settings).\n\nMove fsm-ipc to the FSMONITOR_OS_SETTINGS section in the Makefile, and\nswitch fsm-path-utils to use FSMONITOR_DAEMON_BACKEND since path-utils\nis platform-specific (there will be separate darwin and linux versions).\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Makefile                                      |  6 ++---\n .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  0\n config.mak.uname                              |  2 +-\n contrib/buildsystems/CMakeLists.txt           | 25 +++++++++----------\n meson.build                                   |  7 ++++--\n 5 files changed, 21 insertions(+), 19 deletions(-)\n rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (100%)\n\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..080d009bf0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -417,7 +417,7 @@ include shared.mak\n # If your platform has OS-specific ways to tell if a repo is incompatible with\n # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS\n # to the \"<name>\" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`\n-# that implements the `fsm_os_settings__*()` routines.\n+# and `compat/fsmonitor/fsm-ipc-<name>.c` files.\n #\n # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test\n # programs in oss-fuzz/.\n@@ -2365,13 +2365,13 @@ ifdef FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef FSMONITOR_OS_SETTINGS\n \tCOMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o\n \tCOMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o\n-\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o\n+\tCOMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o\n endif\n \n ifdef WITH_BREAKING_CHANGES\ndiff --git a/compat/fsmonitor/fsm-ipc-darwin.c b/compat/fsmonitor/fsm-ipc-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-ipc-darwin.c\nrename to compat/fsmonitor/fsm-ipc-unix.c\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 3c35ae33a3..33877020e9 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -165,7 +165,7 @@ ifeq ($(uname_S),Darwin)\n         ifndef NO_PTHREADS\n         ifndef NO_UNIX_SOCKETS\n \tFSMONITOR_DAEMON_BACKEND = darwin\n-\tFSMONITOR_OS_SETTINGS = darwin\n+\tFSMONITOR_OS_SETTINGS = unix\n         endif\n         endif\n \ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 28877feb9d..6197d5729c 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -291,23 +291,22 @@ endif()\n \n if(SUPPORTS_SIMPLE_IPC)\n \tif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)\n-\n-\t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"win32\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"win32\")\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\tendif()\n+\n+\tif(FSMONITOR_DAEMON_BACKEND)\n \t\tadd_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..86a68365a9 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1320,10 +1320,13 @@ else\n endif\n \n fsmonitor_backend = ''\n+fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n+  fsmonitor_os = 'win32'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n+  fsmonitor_os = 'unix'\n   libgit_dependencies += dependency('CoreServices')\n endif\n if fsmonitor_backend != ''\n@@ -1332,14 +1335,14 @@ if fsmonitor_backend != ''\n \n   libgit_sources += [\n     'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)\n+build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)\n \n if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''\n   build_options_config.set('NO_REGEX', '')\n-- \ngitgitgadget\n\n"},{"id":"541649","messageId":"46e8c2b74f49d1624f4661f85bfbd9bc2454aa61.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 07/13] fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:31Z","receivedAt":"2026-04-15T13:27:53Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe fsmonitor settings logic in fsm-settings-darwin.c is not\nDarwin-specific and will be reused by the upcoming Linux\nimplementation.  Rename it to fsm-settings-unix.c to reflect that it\nis shared by all Unix platforms.\n\nUpdate the build files (meson.build and CMakeLists.txt) to use\nFSMONITOR_OS_SETTINGS for fsm-settings, matching the approach already\nused for fsm-ipc.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} | 0\n contrib/buildsystems/CMakeLists.txt                             | 2 +-\n meson.build                                                     | 2 +-\n 3 files changed, 2 insertions(+), 2 deletions(-)\n rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)\n\ndiff --git a/compat/fsmonitor/fsm-settings-darwin.c b/compat/fsmonitor/fsm-settings-unix.c\nsimilarity index 100%\nrename from compat/fsmonitor/fsm-settings-darwin.c\nrename to compat/fsmonitor/fsm-settings-unix.c\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 6197d5729c..d613809e26 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -306,7 +306,7 @@ if(SUPPORTS_SIMPLE_IPC)\n \t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)\n \n \t\tadd_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)\n-\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_DAEMON_BACKEND}.c)\n+\t\tlist(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)\n \tendif()\n endif()\n \ndiff --git a/meson.build b/meson.build\nindex 86a68365a9..4f0c0a33b8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1338,7 +1338,7 @@ if fsmonitor_backend != ''\n     'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',\n     'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',\n     'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',\n-    'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',\n+    'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',\n   ]\n endif\n build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)\n-- \ngitgitgadget\n\n"},{"id":"541651","messageId":"b3f40a497b9e28b5189eaa38c98d915376ce5b21.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 08/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:32Z","receivedAt":"2026-04-15T13:27:55Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nImplement the built-in fsmonitor daemon for Linux using the inotify\nAPI, bringing it to feature parity with the existing Windows and macOS\nimplementations.\n\nThe implementation uses inotify rather than fanotify because fanotify\nrequires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it\nunsuitable for an unprivileged user-space daemon.  While inotify has\nthe limitation of requiring a separate watch on every directory (unlike\nmacOS's FSEvents, which can monitor an entire directory tree with a\nsingle watch), it operates without elevated privileges and provides\nthe per-file event granularity needed for fsmonitor.\n\nThe listener uses inotify_init1(O_NONBLOCK) with a poll loop that\nchecks for events with a 50-millisecond timeout, keeping the inotify\nqueue well-drained to minimize the risk of overflows.  Bidirectional\nhashmaps map between watch descriptors and directory paths for efficient\nevent resolution.  Directory renames are tracked using inotify's cookie\nmechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a\nperiodic check detects stale renames where the matching IN_MOVED_TO\nnever arrived, forcing a resync.\n\nNew directory creation triggers recursive watch registration to ensure\nall subdirectories are monitored.  The IN_MASK_CREATE flag is used\nwhere available to prevent modifying existing watches, with a fallback\nfor older kernels.  When IN_MASK_CREATE is available and\ninotify_add_watch returns EEXIST, it means another thread or recursive\nscan has already registered the watch, so it is safe to ignore.\n\nRemote filesystem detection uses statfs() to identify network-mounted\nfilesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers.\nMount point information is read from /proc/mounts and matched against\nthe statfs f_fsid to get accurate, human-readable filesystem type names\nfor logging.  When the .git directory is on a remote filesystem, the\nIPC socket falls back to $HOME or a user-configured directory via the\nfsmonitor.socketDir setting.\n\nBased-on-patch-by: Eric DeCosta <edecosta@mathworks.com>\nBased-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n Documentation/config/fsmonitor--daemon.adoc |   4 +-\n Documentation/git-fsmonitor--daemon.adoc    |  28 +-\n compat/fsmonitor/fsm-health-linux.c         |  33 +\n compat/fsmonitor/fsm-listen-linux.c         | 746 ++++++++++++++++++++\n compat/fsmonitor/fsm-path-utils-linux.c     | 217 ++++++\n config.mak.uname                            |  10 +\n contrib/buildsystems/CMakeLists.txt         |   8 +-\n meson.build                                 |   4 +\n 8 files changed, 1042 insertions(+), 8 deletions(-)\n create mode 100644 compat/fsmonitor/fsm-health-linux.c\n create mode 100644 compat/fsmonitor/fsm-listen-linux.c\n create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c\n\ndiff --git a/Documentation/config/fsmonitor--daemon.adoc b/Documentation/config/fsmonitor--daemon.adoc\nindex 671f9b9462..6f8386e291 100644\n--- a/Documentation/config/fsmonitor--daemon.adoc\n+++ b/Documentation/config/fsmonitor--daemon.adoc\n@@ -4,8 +4,8 @@ fsmonitor.allowRemote::\n     behavior.  Only respected when `core.fsmonitor` is set to `true`.\n \n fsmonitor.socketDir::\n-    This Mac OS-specific option, if set, specifies the directory in\n+    This Mac OS and Linux-specific option, if set, specifies the directory in\n     which to create the Unix domain socket used for communication\n     between the fsmonitor daemon and various Git commands. The directory must\n-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`\n+    reside on a native filesystem.  Only respected when `core.fsmonitor`\n     is set to `true`.\ndiff --git a/Documentation/git-fsmonitor--daemon.adoc b/Documentation/git-fsmonitor--daemon.adoc\nindex 8fe5241b08..12fa866a64 100644\n--- a/Documentation/git-fsmonitor--daemon.adoc\n+++ b/Documentation/git-fsmonitor--daemon.adoc\n@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to\n correctly with all network-mounted repositories, so such use is considered\n experimental.\n \n-On Mac OS, the inter-process communication (IPC) between various Git\n+On Mac OS and Linux, the inter-process communication (IPC) between various Git\n commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a\n-special type of file -- which is supported by native Mac OS filesystems,\n+special type of file -- which is supported by native Mac OS and Linux filesystems,\n but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems\n may or may not have the needed support; the fsmonitor daemon is not guaranteed\n to work with these filesystems and such use is considered experimental.\n@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory.  However, if the\n `.git` directory is on a network-mounted filesystem, it will instead be\n created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a\n network-mounted filesystem, in which case you must set the configuration\n-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native\n+variable `fsmonitor.socketDir` to the path of a directory on a native\n filesystem in which to create the socket file.\n \n If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)\n-is on a native Mac OS file filesystem the fsmonitor daemon will report an\n+is on a native filesystem the fsmonitor daemon will report an\n error that will cause the daemon and the currently running command to exit.\n \n+LINUX CAVEATS\n+~~~~~~~~~~~~~\n+\n+On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.\n+The inotify system has per-user limits on the number of watches that can\n+be created.  The default limit is typically 8192 watches per user.\n+\n+For large repositories with many directories, you may need to increase\n+this limit.  Check the current limit with:\n+\n+    cat /proc/sys/fs/inotify/max_user_watches\n+\n+To temporarily increase the limit:\n+\n+    sudo sysctl fs.inotify.max_user_watches=65536\n+\n+To make the change permanent, add to `/etc/sysctl.conf`:\n+\n+    fs.inotify.max_user_watches=65536\n+\n CONFIGURATION\n -------------\n \ndiff --git a/compat/fsmonitor/fsm-health-linux.c b/compat/fsmonitor/fsm-health-linux.c\nnew file mode 100644\nindex 0000000000..43d67c4b8b\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-health-linux.c\n@@ -0,0 +1,33 @@\n+#include \"git-compat-util.h\"\n+#include \"config.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-health.h\"\n+#include \"fsmonitor--daemon.h\"\n+\n+/*\n+ * The Linux fsmonitor implementation uses inotify which has its own\n+ * mechanisms for detecting filesystem unmount and other events that\n+ * would require the daemon to shutdown.  Therefore, we don't need\n+ * a separate health thread like Windows does.\n+ *\n+ * These stub functions satisfy the interface requirements.\n+ */\n+\n+int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+\treturn;\n+}\n+\n+void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)\n+{\n+}\ndiff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c\nnew file mode 100644\nindex 0000000000..e3dca14b62\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-listen-linux.c\n@@ -0,0 +1,746 @@\n+#include \"git-compat-util.h\"\n+#include \"dir.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsm-listen.h\"\n+#include \"fsmonitor--daemon.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"simple-ipc.h\"\n+#include \"string-list.h\"\n+#include \"trace.h\"\n+\n+#include <sys/inotify.h>\n+\n+/*\n+ * Safe value to bitwise OR with rest of mask for\n+ * kernels that do not support IN_MASK_CREATE\n+ */\n+#ifndef IN_MASK_CREATE\n+#define IN_MASK_CREATE 0x00000000\n+#endif\n+\n+enum shutdown_reason {\n+\tSHUTDOWN_CONTINUE = 0,\n+\tSHUTDOWN_STOP,\n+\tSHUTDOWN_ERROR,\n+\tSHUTDOWN_FORCE\n+};\n+\n+struct watch_entry {\n+\tstruct hashmap_entry ent;\n+\tint wd;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct rename_entry {\n+\tstruct hashmap_entry ent;\n+\ttime_t whence;\n+\tuint32_t cookie;\n+\tconst char *dir;\n+};\n+\n+struct fsm_listen_data {\n+\tint fd_inotify;\n+\tenum shutdown_reason shutdown;\n+\tstruct hashmap watches;\n+\tstruct hashmap renames;\n+\tstruct hashmap revwatches;\n+};\n+\n+static int watch_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t   const struct hashmap_entry *eptr,\n+\t\t\t   const struct hashmap_entry *entry_or_key,\n+\t\t\t   const void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn e1->wd != e2->wd;\n+}\n+\n+static int revwatches_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t\tconst struct hashmap_entry *eptr,\n+\t\t\t\tconst struct hashmap_entry *entry_or_key,\n+\t\t\t\tconst void *keydata UNUSED)\n+{\n+\tconst struct watch_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct watch_entry, ent);\n+\te2 = container_of(entry_or_key, const struct watch_entry, ent);\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static int rename_entry_cmp(const void *cmp_data UNUSED,\n+\t\t\t    const struct hashmap_entry *eptr,\n+\t\t\t    const struct hashmap_entry *entry_or_key,\n+\t\t\t    const void *keydata UNUSED)\n+{\n+\tconst struct rename_entry *e1, *e2;\n+\n+\te1 = container_of(eptr, const struct rename_entry, ent);\n+\te2 = container_of(entry_or_key, const struct rename_entry, ent);\n+\treturn e1->cookie != e2->cookie;\n+}\n+\n+/*\n+ * Register an inotify watch, add watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static int add_watch(const char *path, struct fsm_listen_data *data)\n+{\n+\tconst char *interned = strintern(path);\n+\tstruct watch_entry *w1, *w2;\n+\n+\t/* add the inotify watch, don't allow watches to be modified */\n+\tint wd = inotify_add_watch(data->fd_inotify, interned,\n+\t\t\t\t   (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)\n+\t\t\t\t    ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);\n+\tif (wd < 0) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted or is not a directory */\n+\t\tif (errno == EEXIST)\n+\t\t\treturn 0; /* watch already exists, no action needed */\n+\t\tif (errno == ENOSPC)\n+\t\t\treturn error(_(\"inotify watch limit reached; \"\n+\t\t\t\t       \"increase fs.inotify.max_user_watches\"));\n+\t\treturn error_errno(_(\"inotify_add_watch('%s') failed\"), interned);\n+\t}\n+\n+\t/* add watch descriptor -> directory mapping */\n+\tCALLOC_ARRAY(w1, 1);\n+\tw1->wd = wd;\n+\tw1->dir = interned;\n+\thashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));\n+\thashmap_add(&data->watches, &w1->ent);\n+\n+\t/* add directory -> watch descriptor mapping */\n+\tCALLOC_ARRAY(w2, 1);\n+\tw2->wd = wd;\n+\tw2->dir = interned;\n+\thashmap_entry_init(&w2->ent, strhash(w2->dir));\n+\thashmap_add(&data->revwatches, &w2->ent);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Remove the inotify watch, the watch descriptor to path mapping\n+ * and the reverse mapping.\n+ */\n+static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k1, k2, *w1, *w2;\n+\n+\t/* remove watch, ignore error if kernel already did it */\n+\tif (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)\n+\t\terror_errno(_(\"inotify_rm_watch() failed\"));\n+\n+\tk1.wd = w->wd;\n+\thashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));\n+\tw1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);\n+\tif (!w1)\n+\t\tBUG(\"double remove of watch for '%s'\", w->dir);\n+\n+\tif (w1->cookie)\n+\t\tBUG(\"removing watch for '%s' which has a pending rename\", w1->dir);\n+\n+\tk2.dir = w->dir;\n+\thashmap_entry_init(&k2.ent, strhash(k2.dir));\n+\tw2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);\n+\tif (!w2)\n+\t\tBUG(\"double remove of reverse watch for '%s'\", w->dir);\n+\n+\t/* w1->dir and w2->dir are interned strings, we don't own them */\n+\tfree(w1);\n+\tfree(w2);\n+}\n+\n+/*\n+ * Check for stale directory renames.\n+ *\n+ * https://man7.org/linux/man-pages/man7/inotify.7.html\n+ *\n+ * Allow for some small timeout to account for the fact that insertion of the\n+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that\n+ * there may not be any IN_MOVED_TO event.\n+ *\n+ * If the IN_MOVED_TO event is not received within the timeout then events have\n+ * been missed and the monitor is in an inconsistent state with respect to the\n+ * filesystem.\n+ */\n+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)\n+{\n+\tstruct rename_entry *re;\n+\tstruct hashmap_iter iter;\n+\n+\thashmap_for_each_entry(renames, &iter, re, ent) {\n+\t\tif (re->whence <= max_age)\n+\t\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Track pending renames.\n+ *\n+ * Tracking is done via an event cookie to watch descriptor mapping.\n+ *\n+ * A rename is not complete until matching an IN_MOVED_TO event is received\n+ * for a corresponding IN_MOVED_FROM event.\n+ */\n+static void add_dir_rename(uint32_t cookie, const char *path,\n+\t\t\t   struct fsm_listen_data *data)\n+{\n+\tstruct watch_entry k, *w;\n+\tstruct rename_entry *re;\n+\n+\t/* lookup the watch descriptor for the given path */\n+\tk.dir = path;\n+\thashmap_entry_init(&k.ent, strhash(path));\n+\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\tif (!w) {\n+\t\t/*\n+\t\t * This can happen in rare cases where the directory was\n+\t\t * moved before we had a chance to add a watch on it.\n+\t\t * Just ignore this rename.\n+\t\t */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no watch found for rename from '%s'\", path);\n+\t\treturn;\n+\t}\n+\tw->cookie = cookie;\n+\n+\t/* add the pending rename to match against later */\n+\tCALLOC_ARRAY(re, 1);\n+\tre->dir = w->dir;\n+\tre->cookie = w->cookie;\n+\tre->whence = time(NULL);\n+\thashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));\n+\thashmap_add(&data->renames, &re->ent);\n+}\n+\n+/*\n+ * Handle directory renames\n+ *\n+ * Once an IN_MOVED_TO event is received, lookup the rename tracking information\n+ * via the event cookie and use this information to update the watch.\n+ */\n+static void rename_dir(uint32_t cookie, const char *path,\n+\t\t       struct fsm_listen_data *data)\n+{\n+\tstruct rename_entry rek, *re;\n+\tstruct watch_entry k, *w;\n+\n+\t/* lookup a pending rename to match */\n+\trek.cookie = cookie;\n+\thashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));\n+\tre = hashmap_get_entry(&data->renames, &rek, ent, NULL);\n+\tif (re) {\n+\t\tk.dir = re->dir;\n+\t\thashmap_entry_init(&k.ent, strhash(k.dir));\n+\t\tw = hashmap_get_entry(&data->revwatches, &k, ent, NULL);\n+\t\tif (w) {\n+\t\t\tw->cookie = 0; /* rename handled */\n+\t\t\tremove_watch(w, data);\n+\t\t\tif (add_watch(path, data))\n+\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t \"failed to add watch for renamed dir '%s'\",\n+\t\t\t\t\t\t path);\n+\t\t} else {\n+\t\t\t/* Directory was moved out of watch tree */\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"no matching watch for rename to '%s'\", path);\n+\t\t}\n+\t\thashmap_remove_entry(&data->renames, &rek, ent, NULL);\n+\t\tfree(re);\n+\t} else {\n+\t\t/* Directory was moved from outside the watch tree */\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"no matching cookie for rename to '%s'\", path);\n+\t}\n+}\n+\n+/*\n+ * Recursively add watches to every directory under path\n+ */\n+static int register_inotify(const char *path,\n+\t\t\t    struct fsmonitor_daemon_state *state,\n+\t\t\t    struct fsmonitor_batch *batch)\n+{\n+\tDIR *dir;\n+\tconst char *rel;\n+\tstruct strbuf current = STRBUF_INIT;\n+\tstruct dirent *de;\n+\tstruct stat fs;\n+\tint ret = -1;\n+\n+\tdir = opendir(path);\n+\tif (!dir) {\n+\t\tif (errno == ENOENT || errno == ENOTDIR)\n+\t\t\treturn 0; /* directory was deleted */\n+\t\treturn error_errno(_(\"opendir('%s') failed\"), path);\n+\t}\n+\n+\twhile ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {\n+\t\tstrbuf_reset(&current);\n+\t\tstrbuf_addf(&current, \"%s/%s\", path, de->d_name);\n+\t\tif (lstat(current.buf, &fs)) {\n+\t\t\tif (errno == ENOENT)\n+\t\t\t\tcontinue; /* file was deleted */\n+\t\t\terror_errno(_(\"lstat('%s') failed\"), current.buf);\n+\t\t\tgoto failed;\n+\t\t}\n+\n+\t\t/* recurse into directory */\n+\t\tif (S_ISDIR(fs.st_mode)) {\n+\t\t\tif (add_watch(current.buf, state->listen_data))\n+\t\t\t\tgoto failed;\n+\t\t\tif (register_inotify(current.buf, state, batch))\n+\t\t\t\tgoto failed;\n+\t\t} else if (batch) {\n+\t\t\trel = current.buf + state->path_worktree_watch.len + 1;\n+\t\t\ttrace_printf_key(&trace_fsmonitor, \"explicitly adding '%s'\", rel);\n+\t\t\tfsmonitor_batch__add_path(batch, rel);\n+\t\t}\n+\t}\n+\tret = 0;\n+\n+failed:\n+\tstrbuf_release(&current);\n+\tif (closedir(dir) < 0)\n+\t\treturn error_errno(_(\"closedir('%s') failed\"), path);\n+\treturn ret;\n+}\n+\n+static int em_rename_dir_from(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));\n+}\n+\n+static int em_rename_dir_to(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));\n+}\n+\n+static int em_remove_watch(uint32_t mask)\n+{\n+\treturn (mask & IN_DELETE_SELF);\n+}\n+\n+static int em_dir_renamed(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_MOVE));\n+}\n+\n+static int em_dir_created(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_CREATE));\n+}\n+\n+static int em_dir_deleted(uint32_t mask)\n+{\n+\treturn ((mask & IN_ISDIR) && (mask & IN_DELETE));\n+}\n+\n+static int em_force_shutdown(uint32_t mask)\n+{\n+\treturn (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);\n+}\n+\n+static int em_ignore(uint32_t mask)\n+{\n+\treturn (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);\n+}\n+\n+static void log_mask_set(const char *path, uint32_t mask)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\n+\tif (mask & IN_ACCESS)\n+\t\tstrbuf_addstr(&msg, \"IN_ACCESS|\");\n+\tif (mask & IN_MODIFY)\n+\t\tstrbuf_addstr(&msg, \"IN_MODIFY|\");\n+\tif (mask & IN_ATTRIB)\n+\t\tstrbuf_addstr(&msg, \"IN_ATTRIB|\");\n+\tif (mask & IN_CLOSE_WRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_WRITE|\");\n+\tif (mask & IN_CLOSE_NOWRITE)\n+\t\tstrbuf_addstr(&msg, \"IN_CLOSE_NOWRITE|\");\n+\tif (mask & IN_OPEN)\n+\t\tstrbuf_addstr(&msg, \"IN_OPEN|\");\n+\tif (mask & IN_MOVED_FROM)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_FROM|\");\n+\tif (mask & IN_MOVED_TO)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVED_TO|\");\n+\tif (mask & IN_CREATE)\n+\t\tstrbuf_addstr(&msg, \"IN_CREATE|\");\n+\tif (mask & IN_DELETE)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE|\");\n+\tif (mask & IN_DELETE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_DELETE_SELF|\");\n+\tif (mask & IN_MOVE_SELF)\n+\t\tstrbuf_addstr(&msg, \"IN_MOVE_SELF|\");\n+\tif (mask & IN_UNMOUNT)\n+\t\tstrbuf_addstr(&msg, \"IN_UNMOUNT|\");\n+\tif (mask & IN_Q_OVERFLOW)\n+\t\tstrbuf_addstr(&msg, \"IN_Q_OVERFLOW|\");\n+\tif (mask & IN_IGNORED)\n+\t\tstrbuf_addstr(&msg, \"IN_IGNORED|\");\n+\tif (mask & IN_ISDIR)\n+\t\tstrbuf_addstr(&msg, \"IN_ISDIR|\");\n+\n+\tstrbuf_strip_suffix(&msg, \"|\");\n+\n+\ttrace_printf_key(&trace_fsmonitor, \"inotify_event: '%s', mask=%#8.8x %s\",\n+\t\t\t path, mask, msg.buf);\n+\n+\tstrbuf_release(&msg);\n+}\n+\n+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)\n+{\n+\tint fd;\n+\tint ret = 0;\n+\tstruct fsm_listen_data *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n+\tstate->listen_data = data;\n+\tstate->listen_error_code = -1;\n+\tdata->fd_inotify = -1;\n+\tdata->shutdown = SHUTDOWN_ERROR;\n+\n+\tfd = inotify_init1(O_NONBLOCK);\n+\tif (fd < 0) {\n+\t\tFREE_AND_NULL(state->listen_data);\n+\t\treturn error_errno(_(\"inotify_init1() failed\"));\n+\t}\n+\n+\tdata->fd_inotify = fd;\n+\n+\thashmap_init(&data->watches, watch_entry_cmp, NULL, 0);\n+\thashmap_init(&data->renames, rename_entry_cmp, NULL, 0);\n+\thashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);\n+\n+\tif (add_watch(state->path_worktree_watch.buf, data))\n+\t\tret = -1;\n+\telse if (register_inotify(state->path_worktree_watch.buf, state, NULL))\n+\t\tret = -1;\n+\telse if (state->nr_paths_watching > 1) {\n+\t\tif (add_watch(state->path_gitdir_watch.buf, data))\n+\t\t\tret = -1;\n+\t\telse if (register_inotify(state->path_gitdir_watch.buf, state, NULL))\n+\t\t\tret = -1;\n+\t}\n+\n+\tif (!ret) {\n+\t\tstate->listen_error_code = 0;\n+\t\tdata->shutdown = SHUTDOWN_CONTINUE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)\n+{\n+\tstruct fsm_listen_data *data;\n+\tstruct hashmap_iter iter;\n+\tstruct watch_entry *w;\n+\tstruct watch_entry **to_remove;\n+\tsize_t nr_to_remove = 0, alloc_to_remove = 0;\n+\tsize_t i;\n+\tint fd;\n+\n+\tif (!state || !state->listen_data)\n+\t\treturn;\n+\n+\tdata = state->listen_data;\n+\tfd = data->fd_inotify;\n+\n+\t/*\n+\t * Collect all entries first, then remove them.\n+\t * We can't modify the hashmap while iterating over it.\n+\t */\n+\tto_remove = NULL;\n+\thashmap_for_each_entry(&data->watches, &iter, w, ent) {\n+\t\tALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);\n+\t\tto_remove[nr_to_remove++] = w;\n+\t}\n+\n+\tfor (i = 0; i < nr_to_remove; i++) {\n+\t\tto_remove[i]->cookie = 0; /* ignore any pending renames */\n+\t\tremove_watch(to_remove[i], data);\n+\t}\n+\tfree(to_remove);\n+\n+\thashmap_clear(&data->watches);\n+\n+\thashmap_clear(&data->revwatches); /* remove_watch freed the entries */\n+\n+\thashmap_clear_and_free(&data->renames, struct rename_entry, ent);\n+\n+\tFREE_AND_NULL(state->listen_data);\n+\n+\tif (fd >= 0 && (close(fd) < 0))\n+\t\terror_errno(_(\"closing inotify file descriptor failed\"));\n+}\n+\n+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)\n+{\n+\tif (state && state->listen_data &&\n+\t    state->listen_data->shutdown == SHUTDOWN_CONTINUE)\n+\t\tstate->listen_data->shutdown = SHUTDOWN_STOP;\n+}\n+\n+/*\n+ * Process a single inotify event and queue for publication.\n+ */\n+static int process_event(const char *path,\n+\t\t\t const struct inotify_event *event,\n+\t\t\t struct fsmonitor_batch **batch,\n+\t\t\t struct string_list *cookie_list,\n+\t\t\t struct fsmonitor_daemon_state *state)\n+{\n+\tconst char *rel;\n+\tconst char *last_sep;\n+\n+\tswitch (fsmonitor_classify_path_absolute(state, path)) {\n+\tcase IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:\n+\tcase IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:\n+\t\t/* Use just the filename of the cookie file. */\n+\t\tlast_sep = find_last_dir_sep(path);\n+\t\tstring_list_append(cookie_list,\n+\t\t\t\t   last_sep ? last_sep + 1 : path);\n+\t\tbreak;\n+\tcase IS_INSIDE_DOT_GIT:\n+\tcase IS_INSIDE_GITDIR:\n+\t\tbreak;\n+\tcase IS_DOT_GIT:\n+\tcase IS_GITDIR:\n+\t\t/*\n+\t\t * If .git directory is deleted or renamed away,\n+\t\t * we have to quit.\n+\t\t */\n+\t\tif (em_dir_deleted(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir removed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\tif (em_dir_renamed(event->mask)) {\n+\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t \"event: gitdir renamed\");\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\tgoto done;\n+\t\t}\n+\t\tbreak;\n+\tcase IS_WORKDIR_PATH:\n+\t\t/* normal events in the working directory */\n+\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\tlog_mask_set(path, event->mask);\n+\n+\t\tif (!*batch)\n+\t\t\t*batch = fsmonitor_batch__new();\n+\n+\t\trel = path + state->path_worktree_watch.len + 1;\n+\t\tfsmonitor_batch__add_path(*batch, rel);\n+\n+\t\tif (em_dir_deleted(event->mask))\n+\t\t\tbreak;\n+\n+\t\t/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */\n+\t\tif (em_rename_dir_from(event->mask))\n+\t\t\tadd_dir_rename(event->cookie, path, state->listen_data);\n+\n+\t\t/* received IN_MOVE_TO, update watch to reflect new path */\n+\t\tif (em_rename_dir_to(event->mask)) {\n+\t\t\trename_dir(event->cookie, path, state->listen_data);\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (em_dir_created(event->mask)) {\n+\t\t\tif (add_watch(path, state->listen_data)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tif (register_inotify(path, state, *batch)) {\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+\tcase IS_OUTSIDE_CONE:\n+\tdefault:\n+\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t \"ignoring '%s'\", path);\n+\t\tbreak;\n+\t}\n+\treturn 0;\n+done:\n+\treturn -1;\n+}\n+\n+/*\n+ * Read the inotify event stream and pre-process events before further\n+ * processing and eventual publishing.\n+ */\n+static void handle_events(struct fsmonitor_daemon_state *state)\n+{\n+\t/* See https://man7.org/linux/man-pages/man7/inotify.7.html */\n+\tchar buf[4096]\n+\t\t__attribute__ ((aligned(__alignof__(struct inotify_event))));\n+\n+\tstruct hashmap *watches = &state->listen_data->watches;\n+\tstruct fsmonitor_batch *batch = NULL;\n+\tstruct string_list cookie_list = STRING_LIST_INIT_DUP;\n+\tstruct watch_entry k, *w;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst struct inotify_event *event;\n+\tint fd = state->listen_data->fd_inotify;\n+\tssize_t len;\n+\tchar *ptr, *p;\n+\n+\tfor (;;) {\n+\t\tlen = read(fd, buf, sizeof(buf));\n+\t\tif (len == -1) {\n+\t\t\tif (errno == EAGAIN || errno == EINTR)\n+\t\t\t\tgoto done;\n+\t\t\terror_errno(_(\"reading inotify message stream failed\"));\n+\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\tgoto done;\n+\t\t}\n+\n+\t\t/* nothing to read */\n+\t\tif (len == 0)\n+\t\t\tgoto done;\n+\n+\t\t/* Loop over all events in the buffer. */\n+\t\tfor (ptr = buf; ptr < buf + len;\n+\t\t     ptr += sizeof(struct inotify_event) + event->len) {\n+\n+\t\t\tevent = (const struct inotify_event *)ptr;\n+\n+\t\t\tif (em_ignore(event->mask))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/* File system was unmounted or event queue overflowed */\n+\t\t\tif (em_force_shutdown(event->mask)) {\n+\t\t\t\tif (trace_pass_fl(&trace_fsmonitor))\n+\t\t\t\t\tlog_mask_set(\"forcing shutdown\", event->mask);\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\n+\t\t\tk.wd = event->wd;\n+\t\t\thashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));\n+\n+\t\t\tw = hashmap_get_entry(watches, &k, ent, NULL);\n+\t\t\tif (!w) {\n+\t\t\t\t/* Watch was removed, skip event */\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\t/* directory watch was removed */\n+\t\t\tif (em_remove_watch(event->mask)) {\n+\t\t\t\tremove_watch(w, state->listen_data);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tstrbuf_reset(&path);\n+\t\t\tstrbuf_addf(&path, \"%s/%s\", w->dir, event->name);\n+\n+\t\t\tp = fsmonitor__resolve_alias(path.buf, &state->alias);\n+\t\t\tif (!p)\n+\t\t\t\tp = strbuf_detach(&path, NULL);\n+\n+\t\t\tif (process_event(p, event, &batch, &cookie_list, state)) {\n+\t\t\t\tfree(p);\n+\t\t\t\tgoto done;\n+\t\t\t}\n+\t\t\tfree(p);\n+\t\t}\n+\t\tstrbuf_reset(&path);\n+\t\tfsmonitor_publish(state, batch, &cookie_list);\n+\t\tstring_list_clear(&cookie_list, 0);\n+\t\tbatch = NULL;\n+\t}\n+done:\n+\tstrbuf_release(&path);\n+\tfsmonitor_batch__free_list(batch);\n+\tstring_list_clear(&cookie_list, 0);\n+}\n+\n+/*\n+ * Non-blocking read of the inotify events stream. The inotify fd is polled\n+ * frequently to help minimize the number of queue overflows.\n+ */\n+void fsm_listen__loop(struct fsmonitor_daemon_state *state)\n+{\n+\tint poll_num;\n+\t/*\n+\t * Interval in seconds between checks for stale directory renames.\n+\t * A directory rename that is not completed within this window\n+\t * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates\n+\t * missed events, forcing a shutdown.\n+\t */\n+\tconst int interval = 1;\n+\ttime_t checked = time(NULL);\n+\tstruct pollfd fds[1];\n+\n+\tfds[0].fd = state->listen_data->fd_inotify;\n+\tfds[0].events = POLLIN;\n+\n+\t/*\n+\t * Our fs event listener is now running, so it's safe to start\n+\t * serving client requests.\n+\t */\n+\tipc_server_start_async(state->ipc_server_data);\n+\n+\tfor (;;) {\n+\t\tswitch (state->listen_data->shutdown) {\n+\t\tcase SHUTDOWN_CONTINUE:\n+\t\t\tpoll_num = poll(fds, 1, 50);\n+\t\t\tif (poll_num == -1) {\n+\t\t\t\tif (errno == EINTR)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terror_errno(_(\"polling inotify message stream failed\"));\n+\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_ERROR;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif ((time(NULL) - checked) >= interval) {\n+\t\t\t\tchecked = time(NULL);\n+\t\t\t\tif (check_stale_dir_renames(&state->listen_data->renames,\n+\t\t\t\t\t\t\t    checked - interval)) {\n+\t\t\t\t\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t\t\t\t\t \"missed IN_MOVED_TO events, forcing shutdown\");\n+\t\t\t\t\tstate->listen_data->shutdown = SHUTDOWN_FORCE;\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (poll_num > 0 && (fds[0].revents & POLLIN))\n+\t\t\t\thandle_events(state);\n+\n+\t\t\tcontinue;\n+\t\tcase SHUTDOWN_ERROR:\n+\t\t\tstate->listen_error_code = -1;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_FORCE:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tipc_server_stop_async(state->ipc_server_data);\n+\t\t\tbreak;\n+\t\tcase SHUTDOWN_STOP:\n+\t\tdefault:\n+\t\t\tstate->listen_error_code = 0;\n+\t\t\tbreak;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\ndiff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c\nnew file mode 100644\nindex 0000000000..c9866b1b24\n--- /dev/null\n+++ b/compat/fsmonitor/fsm-path-utils-linux.c\n@@ -0,0 +1,217 @@\n+#include \"git-compat-util.h\"\n+#include \"fsmonitor-ll.h\"\n+#include \"fsmonitor-path-utils.h\"\n+#include \"gettext.h\"\n+#include \"trace.h\"\n+\n+#include <sys/statfs.h>\n+\n+#ifdef HAVE_LINUX_MAGIC_H\n+#include <linux/magic.h>\n+#endif\n+\n+/*\n+ * Filesystem magic numbers for remote filesystems.\n+ * Defined here if not available in linux/magic.h.\n+ */\n+#ifndef CIFS_SUPER_MAGIC\n+#define CIFS_SUPER_MAGIC 0xff534d42\n+#endif\n+#ifndef SMB_SUPER_MAGIC\n+#define SMB_SUPER_MAGIC 0x517b\n+#endif\n+#ifndef SMB2_SUPER_MAGIC\n+#define SMB2_SUPER_MAGIC 0xfe534d42\n+#endif\n+#ifndef NFS_SUPER_MAGIC\n+#define NFS_SUPER_MAGIC 0x6969\n+#endif\n+#ifndef AFS_SUPER_MAGIC\n+#define AFS_SUPER_MAGIC 0x5346414f\n+#endif\n+#ifndef CODA_SUPER_MAGIC\n+#define CODA_SUPER_MAGIC 0x73757245\n+#endif\n+#ifndef FUSE_SUPER_MAGIC\n+#define FUSE_SUPER_MAGIC 0x65735546\n+#endif\n+\n+/*\n+ * Check if filesystem type is a remote filesystem.\n+ */\n+static int is_remote_fs(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\tcase SMB_SUPER_MAGIC:\n+\tcase SMB2_SUPER_MAGIC:\n+\tcase NFS_SUPER_MAGIC:\n+\tcase AFS_SUPER_MAGIC:\n+\tcase CODA_SUPER_MAGIC:\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn 1;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+/*\n+ * Map filesystem magic numbers to human-readable names as a fallback\n+ * when /proc/mounts is unavailable.  This only covers the remote and\n+ * special filesystems in is_remote_fs() above; local filesystems are\n+ * never flagged as incompatible, so we do not need their names here.\n+ */\n+static const char *get_fs_typename(unsigned long f_type)\n+{\n+\tswitch (f_type) {\n+\tcase CIFS_SUPER_MAGIC:\n+\t\treturn \"cifs\";\n+\tcase SMB_SUPER_MAGIC:\n+\t\treturn \"smb\";\n+\tcase SMB2_SUPER_MAGIC:\n+\t\treturn \"smb2\";\n+\tcase NFS_SUPER_MAGIC:\n+\t\treturn \"nfs\";\n+\tcase AFS_SUPER_MAGIC:\n+\t\treturn \"afs\";\n+\tcase CODA_SUPER_MAGIC:\n+\t\treturn \"coda\";\n+\tcase FUSE_SUPER_MAGIC:\n+\t\treturn \"fuse\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+/*\n+ * Find the mount point for a given path by reading /proc/mounts.\n+ *\n+ * statfs(2) gives us f_type (the magic number) but not the human-readable\n+ * filesystem type string.  We scan /proc/mounts to find the mount entry\n+ * whose path is the longest prefix of ours and whose f_fsid matches,\n+ * which gives us the fstype string (e.g. \"nfs\", \"ext4\") for logging.\n+ */\n+static char *find_mount(const char *path, const struct statfs *path_fs)\n+{\n+\tFILE *fp;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf match = STRBUF_INIT;\n+\tstruct strbuf fstype = STRBUF_INIT;\n+\tchar *result = NULL;\n+\n+\tfp = fopen(\"/proc/mounts\", \"r\");\n+\tif (!fp)\n+\t\treturn NULL;\n+\n+\twhile (strbuf_getline(&line, fp) != EOF) {\n+\t\tchar *fields[6];\n+\t\tchar *p = line.buf;\n+\t\tint i;\n+\n+\t\t/* Parse mount entry: device mountpoint fstype options dump pass */\n+\t\tfor (i = 0; i < 6 && p; i++) {\n+\t\t\tfields[i] = p;\n+\t\t\tp = strchr(p, ' ');\n+\t\t\tif (p)\n+\t\t\t\t*p++ = '\\0';\n+\t\t}\n+\n+\t\tif (i >= 3) {\n+\t\t\tconst char *mountpoint = fields[1];\n+\t\t\tconst char *type = fields[2];\n+\t\t\tstruct statfs mount_fs;\n+\n+\t\t\t/* Check if this mount point is a prefix of our path */\n+\t\t\tif (starts_with(path, mountpoint) &&\n+\t\t\t    (path[strlen(mountpoint)] == '/' ||\n+\t\t\t     path[strlen(mountpoint)] == '\\0')) {\n+\t\t\t\t/* Check if filesystem ID matches */\n+\t\t\t\tif (statfs(mountpoint, &mount_fs) == 0 &&\n+\t\t\t\t    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,\n+\t\t\t\t\t    sizeof(mount_fs.f_fsid))) {\n+\t\t\t\t\t/* Keep the longest matching mount point */\n+\t\t\t\t\tif (strlen(mountpoint) > match.len) {\n+\t\t\t\t\t\tstrbuf_reset(&match);\n+\t\t\t\t\t\tstrbuf_addstr(&match, mountpoint);\n+\t\t\t\t\t\tstrbuf_reset(&fstype);\n+\t\t\t\t\t\tstrbuf_addstr(&fstype, type);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfclose(fp);\n+\tstrbuf_release(&line);\n+\tstrbuf_release(&match);\n+\n+\tif (fstype.len)\n+\t\tresult = strbuf_detach(&fstype, NULL);\n+\telse\n+\t\tstrbuf_release(&fstype);\n+\n+\treturn result;\n+}\n+\n+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)\n+{\n+\tstruct statfs fs;\n+\n+\tif (statfs(path, &fs) == -1) {\n+\t\tint saved_errno = errno;\n+\t\ttrace_printf_key(&trace_fsmonitor, \"statfs('%s') failed: %s\",\n+\t\t\t\t path, strerror(saved_errno));\n+\t\terrno = saved_errno;\n+\t\treturn -1;\n+\t}\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"statfs('%s') [type 0x%08lx]\",\n+\t\t\t path, (unsigned long)fs.f_type);\n+\n+\tfs_info->is_remote = is_remote_fs(fs.f_type);\n+\n+\t/*\n+\t * Try to get filesystem type from /proc/mounts for a more\n+\t * descriptive name.\n+\t */\n+\tfs_info->typename = find_mount(path, &fs);\n+\tif (!fs_info->typename)\n+\t\tfs_info->typename = xstrdup(get_fs_typename(fs.f_type));\n+\n+\ttrace_printf_key(&trace_fsmonitor,\n+\t\t\t \"'%s' is_remote: %d, typename: %s\",\n+\t\t\t path, fs_info->is_remote, fs_info->typename);\n+\n+\treturn 0;\n+}\n+\n+int fsmonitor__is_fs_remote(const char *path)\n+{\n+\tstruct fs_info fs;\n+\n+\tif (fsmonitor__get_fs_info(path, &fs))\n+\t\treturn -1;\n+\n+\tfree(fs.typename);\n+\n+\treturn fs.is_remote;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+int fsmonitor__get_alias(const char *path UNUSED,\n+\t\t\t struct alias_info *info UNUSED)\n+{\n+\treturn 0;\n+}\n+\n+/*\n+ * No-op for Linux - we don't have firmlinks like macOS.\n+ */\n+char *fsmonitor__resolve_alias(const char *path UNUSED,\n+\t\t\t       const struct alias_info *info UNUSED)\n+{\n+\treturn NULL;\n+}\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 33877020e9..638f7e1bde 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)\n \t\tBASIC_CFLAGS += -std=c99\n         endif\n \tLINK_FUZZ_PROGRAMS = YesPlease\n+\n+\t# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require\n+\t# Unix domain sockets and PThreads.\n+        ifndef NO_PTHREADS\n+        ifndef NO_UNIX_SOCKETS\n+\tFSMONITOR_DAEMON_BACKEND = linux\n+\tFSMONITOR_OS_SETTINGS = unix\n+\tBASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H\n+        endif\n+        endif\n endif\n ifeq ($(uname_S),GNU/kFreeBSD)\n \tHAVE_ALLOCA_H = YesPlease\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex d613809e26..b7da108f29 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)\n \telseif(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")\n \t\tset(FSMONITOR_DAEMON_BACKEND \"darwin\")\n \t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\telseif(CMAKE_SYSTEM_NAME STREQUAL \"Linux\")\n+\t\tset(FSMONITOR_DAEMON_BACKEND \"linux\")\n+\t\tset(FSMONITOR_OS_SETTINGS \"unix\")\n+\t\tadd_compile_definitions(HAVE_LINUX_MAGIC_H)\n \tendif()\n \n \tif(FSMONITOR_DAEMON_BACKEND)\n@@ -1149,8 +1153,8 @@ endif()\n file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)\n string(REPLACE \"@BROKEN_PATH_FIX@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@DIFF@\" \"'${DIFF}'\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"win32\" git_build_options \"${git_build_options}\")\n-string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"win32\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_DAEMON_BACKEND@\" \"${FSMONITOR_DAEMON_BACKEND}\" git_build_options \"${git_build_options}\")\n+string(REPLACE \"@FSMONITOR_OS_SETTINGS@\" \"${FSMONITOR_OS_SETTINGS}\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GITWEBDIR@\" \"'${GITWEBDIR}'\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_INTEROP_MAKE_OPTS@\" \"\" git_build_options \"${git_build_options}\")\n string(REPLACE \"@GIT_PERF_LARGE_REPO@\" \"\" git_build_options \"${git_build_options}\")\ndiff --git a/meson.build b/meson.build\nindex 4f0c0a33b8..123d218460 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1324,6 +1324,10 @@ fsmonitor_os = ''\n if host_machine.system() == 'windows'\n   fsmonitor_backend = 'win32'\n   fsmonitor_os = 'win32'\n+elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')\n+  fsmonitor_backend = 'linux'\n+  fsmonitor_os = 'unix'\n+  libgit_c_args += '-DHAVE_LINUX_MAGIC_H'\n elif host_machine.system() == 'darwin'\n   fsmonitor_backend = 'darwin'\n   fsmonitor_os = 'unix'\n-- \ngitgitgadget\n\n"},{"id":"541650","messageId":"5791edbef245a00c158a8c865a0a9eb052151fbb.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 09/13] run-command: add close_fd_above_stderr option","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:33Z","receivedAt":"2026-04-15T13:27:56Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a close_fd_above_stderr flag to struct child_process.  When set,\nthe child closes file descriptors 3 and above between fork and exec\n(skipping the child-notifier pipe), capped at sysconf(_SC_OPEN_MAX)\nor 4096, whichever is smaller.  This prevents the child from\ninheriting pipe endpoints or other descriptors from the parent\nenvironment (e.g., the test harness).\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n run-command.c | 12 ++++++++++++\n run-command.h |  9 +++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/run-command.c b/run-command.c\nindex e3e02475cc..f4361906c9 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -546,6 +546,7 @@ static void atfork_parent(struct atfork_state *as)\n \t\t\"restoring signal mask\");\n #endif\n }\n+\n #endif /* GIT_WINDOWS_NATIVE */\n \n static inline void set_cloexec(int fd)\n@@ -832,6 +833,17 @@ fail_pipe:\n \t\t\tchild_close(cmd->out);\n \t\t}\n \n+\t\tif (cmd->close_fd_above_stderr) {\n+\t\t\tlong max_fd = sysconf(_SC_OPEN_MAX);\n+\t\t\tint fd;\n+\t\t\tif (max_fd < 0 || max_fd > 4096)\n+\t\t\t\tmax_fd = 4096;\n+\t\t\tfor (fd = 3; fd < max_fd; fd++) {\n+\t\t\t\tif (fd != child_notifier)\n+\t\t\t\t\tclose(fd);\n+\t\t\t}\n+\t\t}\n+\n \t\tif (cmd->dir && chdir(cmd->dir))\n \t\t\tchild_die(CHILD_ERR_CHDIR);\n \ndiff --git a/run-command.h b/run-command.h\nindex 0df25e445f..fdaa01e140 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -141,6 +141,15 @@ struct child_process {\n \tunsigned stdout_to_stderr:1;\n \tunsigned clean_on_exit:1;\n \tunsigned wait_after_clean:1;\n+\n+\t/**\n+\t * Close file descriptors 3 and above in the child after forking\n+\t * but before exec.  This prevents the child from inheriting\n+\t * pipe endpoints or other descriptors from the parent\n+\t * environment (e.g., the test harness).\n+\t */\n+\tunsigned close_fd_above_stderr:1;\n+\n \tvoid (*clean_on_exit_handler)(struct child_process *process);\n };\n \n-- \ngitgitgadget\n\n"},{"id":"541652","messageId":"22d425ebebf3a81b0e89caddf4fe1b4a94005c58.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 10/13] fsmonitor: close inherited file descriptors and detach in daemon","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:34Z","receivedAt":"2026-04-15T13:27:58Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nWhen the fsmonitor daemon is spawned as a background process, it may\ninherit file descriptors from its parent that it does not need.  In\nparticular, when the test harness or a CI system captures output through\npipes, the daemon can inherit duplicated pipe endpoints.  If the daemon\nholds these open, the parent process never sees EOF and may appear to\nhang.\n\nSet close_fd_above_stderr on the child process at both daemon startup\npaths: the explicit \"fsmonitor--daemon start\" command and the implicit\nspawn triggered by fsmonitor-ipc when a client finds no running daemon.\nAlso suppress stdout and stderr on the implicit spawn path to prevent\nthe background daemon from writing to the client's terminal.\n\nAdditionally, call setsid() when the daemon starts with --detach to\ncreate a new session and process group.  This prevents the daemon\nfrom being part of the spawning shell's process group, which could\ncause the shell's \"wait\" to block until the daemon exits.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 16 ++++++++++++++--\n fsmonitor-ipc.c             |  3 +++\n 2 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex c8ec7b722e..b2a816dc3f 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -1439,7 +1439,7 @@ done:\n \treturn err;\n }\n \n-static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n+static int try_to_run_foreground_daemon(int detach_console)\n {\n \t/*\n \t * Technically, we don't need to probe for an existing daemon\n@@ -1459,10 +1459,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)\n \t\tfflush(stderr);\n \t}\n \n+\tif (detach_console) {\n #ifdef GIT_WINDOWS_NATIVE\n-\tif (detach_console)\n \t\tFreeConsole();\n+#else\n+\t\t/*\n+\t\t * Create a new session so that the daemon is detached\n+\t\t * from the parent's process group.  This prevents\n+\t\t * shells with job control (e.g. bash with \"set -m\")\n+\t\t * from waiting on the daemon when they wait for a\n+\t\t * foreground command that implicitly spawned it.\n+\t\t */\n+\t\tif (setsid() == -1)\n+\t\t\twarning_errno(_(\"setsid failed\"));\n #endif\n+\t}\n \n \treturn !!fsmonitor_run_daemon();\n }\n@@ -1525,6 +1536,7 @@ static int try_to_start_background_daemon(void)\n \tcp.no_stdin = 1;\n \tcp.no_stdout = 1;\n \tcp.no_stderr = 1;\n+\tcp.close_fd_above_stderr = 1;\n \n \tsbgr = start_bg_command(&cp, bg_wait_cb, NULL,\n \t\t\t\tfsmonitor__start_timeout_sec);\ndiff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c\nindex f1b1631111..6112d13064 100644\n--- a/fsmonitor-ipc.c\n+++ b/fsmonitor-ipc.c\n@@ -61,6 +61,9 @@ static int spawn_daemon(void)\n \n \tcmd.git_cmd = 1;\n \tcmd.no_stdin = 1;\n+\tcmd.no_stdout = 1;\n+\tcmd.no_stderr = 1;\n+\tcmd.close_fd_above_stderr = 1;\n \tcmd.trace2_child_class = \"fsmonitor\";\n \tstrvec_pushl(&cmd.args, \"fsmonitor--daemon\", \"start\", NULL);\n \n-- \ngitgitgadget\n\n"},{"id":"541653","messageId":"fd6bdc8c55cc1acc07586815f09925e4ad6796c3.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 11/13] fsmonitor: add timeout to daemon stop command","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:35Z","receivedAt":"2026-04-15T13:28:00Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nThe \"fsmonitor--daemon stop\" command polls in a loop waiting for the\ndaemon to exit after sending a \"quit\" command over IPC.  If the daemon\nfails to shut down (e.g. it is stuck or wedged), this loop spins\nforever.\n\nAdd a 30-second timeout so the stop command returns an error instead\nof blocking indefinitely.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 12 +++++++++++-\n 1 file changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex b2a816dc3f..53d8ad1f0d 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)\n {\n \tstruct strbuf answer = STRBUF_INIT;\n \tint ret;\n+\tint max_wait_ms = 30000;\n+\tint elapsed_ms = 0;\n \n \tret = fsmonitor_ipc__send_command(\"quit\", &answer);\n \n@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)\n \t\treturn ret;\n \n \ttrace2_region_enter(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n-\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)\n+\twhile (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {\n+\t\tif (elapsed_ms >= max_wait_ms) {\n+\t\t\ttrace2_region_leave(\"fsm_client\",\n+\t\t\t\t\t    \"polling-for-daemon-exit\", NULL);\n+\t\t\treturn error(_(\"daemon did not stop within %d seconds\"),\n+\t\t\t\t     max_wait_ms / 1000);\n+\t\t}\n \t\tsleep_millisec(50);\n+\t\telapsed_ms += 50;\n+\t}\n \ttrace2_region_leave(\"fsm_client\", \"polling-for-daemon-exit\", NULL);\n \n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"541654","messageId":"ffffd64a4baab2c6884cb07cebdc40e5e7c4f0a2.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 12/13] fsmonitor: add tests for Linux","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:36Z","receivedAt":"2026-04-15T13:28:02Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nAdd a smoke test that verifies the filesystem actually delivers\ninotify events to the daemon.  On some configurations (e.g.,\noverlayfs with older kernels), inotify watches succeed but events\nare never delivered.  The daemon cookie wait will time out, but\nevery subsequent test would fail.  Skip the entire test file early\nwhen this is detected.\n\nAdd a test that exercises rapid nested directory creation to verify\nthe daemon correctly handles the EEXIST race between recursive scan\nand queued inotify events.  When IN_MASK_CREATE is available and a\ndirectory watch is added during recursive registration, the kernel\nmay also deliver a queued IN_CREATE event for the same directory.\nThe second inotify_add_watch() returns EEXIST, which must be treated\nas harmless.  An earlier version of the listener crashed in this\nscenario.\n\nReduce --start-timeout from the default 60 seconds to 10 seconds so\nthat tests fail promptly when the daemon cannot start.\n\nHarden the test helpers to work in environments without procps\n(e.g., Fedora CI): fall back to reading /proc/$pid/stat for the\nprocess group ID when ps is unavailable, guard stop_git() against\nan empty pgid, and redirect stderr from kill to /dev/null to avoid\nnoise when processes have already exited.\n\nUse set -m to enable job control in the submodule-pull test so that\nthe background git pull gets its own process group, preventing the\nshell wait from blocking on the daemon.  setsid() in the previous\ncommit detaches the daemon itself, but the intermediate git pull\nprocess still needs its own process group for the test shell to\nmanage it correctly.\n\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n t/t7527-builtin-fsmonitor.sh | 88 +++++++++++++++++++++++++++++++++---\n 1 file changed, 81 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..ed12f218de 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -10,9 +10,57 @@ then\n \ttest_done\n fi\n \n+# Verify that the filesystem delivers events to the daemon.\n+# On some configurations (e.g., overlayfs with older kernels),\n+# inotify watches succeed but events are never delivered.  The\n+# cookie wait will time out and the daemon logs a trace message.\n+#\n+# Use \"timeout\" (if available) to guard each step against hangs.\n+maybe_timeout () {\n+\tif type timeout >/dev/null 2>&1\n+\tthen\n+\t\ttimeout \"$@\"\n+\telse\n+\t\tshift\n+\t\t\"$@\"\n+\tfi\n+}\n+verify_fsmonitor_works () {\n+\tgit init test_fsmonitor_smoke || return 1\n+\n+\tGIT_TRACE_FSMONITOR=\"$PWD/smoke.trace\" &&\n+\texport GIT_TRACE_FSMONITOR &&\n+\tmaybe_timeout 30 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon start \\\n+\t\t\t--start-timeout=10\n+\tret=$?\n+\tunset GIT_TRACE_FSMONITOR\n+\tif test $ret -ne 0\n+\tthen\n+\t\trm -rf test_fsmonitor_smoke smoke.trace\n+\t\treturn 1\n+\tfi\n+\n+\tmaybe_timeout 10 \\\n+\t\ttest-tool -C test_fsmonitor_smoke fsmonitor-client query \\\n+\t\t\t--token 0 >/dev/null 2>&1\n+\tmaybe_timeout 5 \\\n+\t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n+\tret=$?\n+\trm -rf test_fsmonitor_smoke smoke.trace\n+\treturn $ret\n+}\n+\n+if ! verify_fsmonitor_works\n+then\n+\tskip_all=\"filesystem does not deliver fsmonitor events (container/overlayfs?)\"\n+\ttest_done\n+fi\n+\n stop_daemon_delete_repo () {\n \tr=$1 &&\n-\ttest_might_fail git -C $r fsmonitor--daemon stop &&\n+\t{ maybe_timeout 30 git -C $r fsmonitor--daemon stop 2>/dev/null || :; } &&\n \trm -rf $1\n }\n \n@@ -67,7 +115,7 @@ start_daemon () {\n \t\t\texport GIT_TEST_FSMONITOR_TOKEN\n \t\tfi &&\n \n-\t\tgit $r fsmonitor--daemon start &&\n+\t\tgit $r fsmonitor--daemon start --start-timeout=10 &&\n \t\tgit $r fsmonitor--daemon status\n \t)\n }\n@@ -520,6 +568,28 @@ test_expect_success 'directory changes to a file' '\n \tgrep \"^event: dir1$\" .git/trace\n '\n \n+test_expect_success 'rapid nested directory creation' '\n+\ttest_when_finished \"git fsmonitor--daemon stop; rm -rf rapid\" &&\n+\n+\tstart_daemon --tf \"$PWD/.git/trace\" &&\n+\n+\t# Rapidly create nested directories to exercise race conditions\n+\t# where directory watches may be added concurrently during\n+\t# event processing and recursive scanning.\n+\tfor i in $(test_seq 1 20)\n+\tdo\n+\t\tmkdir -p \"rapid/nested/dir$i/subdir/deep\" || return 1\n+\tdone &&\n+\n+\t# Give the daemon time to process all events\n+\tsleep 1 &&\n+\n+\ttest-tool fsmonitor-client query --token 0 &&\n+\n+\t# Verify daemon is still running (did not crash)\n+\tgit fsmonitor--daemon status\n+'\n+\n # The next few test cases exercise the token-resync code.  When filesystem\n # drops events (because of filesystem velocity or because the daemon isn't\n # polling fast enough), we need to discard the cached data (relative to the\n@@ -910,7 +980,10 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n start_git_in_background () {\n \tgit \"$@\" &\n \tgit_pid=$!\n-\tgit_pgid=$(ps -o pgid= -p $git_pid)\n+\tgit_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||\n+\t\tawk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&\n+\tgit_pgid=\"${git_pgid## }\" &&\n+\tgit_pgid=\"${git_pgid%% }\"\n \tnr_tries_left=10\n \twhile true\n \tdo\n@@ -921,15 +994,16 @@ start_git_in_background () {\n \t\tfi\n \t\tsleep 1\n \t\tnr_tries_left=$(($nr_tries_left - 1))\n-\tdone >/dev/null 2>&1 &\n+\tdone >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &\n \twatchdog_pid=$!\n \twait $git_pid\n }\n \n stop_git () {\n-\twhile kill -0 -- -$git_pgid\n+\ttest -n \"$git_pgid\" || return 0\n+\twhile kill -0 -- -$git_pgid 2>/dev/null\n \tdo\n-\t\tkill -- -$git_pgid\n+\t\tkill -- -$git_pgid 2>/dev/null\n \t\tsleep 1\n \tdone\n }\n@@ -944,7 +1018,7 @@ stop_watchdog () {\n \n test_expect_success !MINGW \"submodule implicitly starts daemon by pull\" '\n \ttest_atexit \"stop_watchdog\" &&\n-\ttest_when_finished \"stop_git; rm -rf cloned super sub\" &&\n+\ttest_when_finished \"set +m; stop_git; rm -rf cloned super sub\" &&\n \n \tcreate_super super &&\n \tcreate_sub sub &&\n-- \ngitgitgadget\n\n"},{"id":"541655","messageId":"ada3f1888bca69347406aa8188714399d819b3d8.1776259657.git.gitgitgadget@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"[PATCH v15 13/13] fsmonitor: convert shown khash to strset in do_handle_client","fromName":"Paul Tarjan via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-15T13:27:37Z","receivedAt":"2026-04-15T13:28:03Z","isPatch":true,"body":"From: Paul Tarjan <github@paulisageek.com>\n\nReplace the khash-based string set used for deduplicating pathnames\nin do_handle_client() with a strset, which provides a cleaner\ninterface for the same purpose.\n\nSince the paths are interned strings from the batch data, use\nstrdup_strings=0 to avoid unnecessary copies.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Paul Tarjan <github@paulisageek.com>\n---\n builtin/fsmonitor--daemon.c | 17 ++++++-----------\n 1 file changed, 6 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c\nindex 53d8ad1f0d..f920cf3a82 100644\n--- a/builtin/fsmonitor--daemon.c\n+++ b/builtin/fsmonitor--daemon.c\n@@ -16,7 +16,7 @@\n #include \"fsmonitor--daemon.h\"\n \n #include \"simple-ipc.h\"\n-#include \"khash.h\"\n+#include \"strmap.h\"\n #include \"run-command.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n@@ -674,8 +674,6 @@ static int fsmonitor_parse_client_token(const char *buf_token,\n \treturn 0;\n }\n \n-KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)\n-\n static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\t    const char *command,\n \t\t\t    ipc_server_reply_cb *reply,\n@@ -692,8 +690,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \tconst struct fsmonitor_batch *batch;\n \tstruct fsmonitor_batch *remainder = NULL;\n \tintmax_t count = 0, duplicates = 0;\n-\tkh_str_t *shown = NULL;\n-\tint hash_ret;\n+\tstruct strset shown = STRSET_INIT;\n \tint do_trivial = 0;\n \tint do_flush = 0;\n \tint do_cookie = 0;\n@@ -882,14 +879,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t * so walk the batch list backwards from the current head back\n \t * to the batch (sequence number) they named.\n \t *\n-\t * We use khash to de-dup the list of pathnames.\n+\t * We use a strset to de-dup the list of pathnames.\n \t *\n \t * NEEDSWORK: each batch contains a list of interned strings,\n \t * so we only need to do pointer comparisons here to build the\n \t * hash table.  Currently, we're still comparing the string\n \t * values.\n \t */\n-\tshown = kh_init_str();\n+\tstrset_init_with_options(&shown, NULL, 0);\n \tfor (batch = batch_head;\n \t     batch && batch->batch_seq_nr > requested_oldest_seq_nr;\n \t     batch = batch->next) {\n@@ -899,11 +896,9 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \t\t\tconst char *s = batch->interned_paths[k];\n \t\t\tsize_t s_len;\n \n-\t\t\tif (kh_get_str(shown, s) != kh_end(shown))\n+\t\t\tif (!strset_add(&shown, s))\n \t\t\t\tduplicates++;\n \t\t\telse {\n-\t\t\t\tkh_put_str(shown, s, &hash_ret);\n-\n \t\t\t\ttrace_printf_key(&trace_fsmonitor,\n \t\t\t\t\t\t \"send[%\"PRIuMAX\"]: %s\",\n \t\t\t\t\t\t count, s);\n@@ -973,7 +968,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,\n \ttrace2_data_intmax(\"fsmonitor\", the_repository, \"response/count/duplicates\", duplicates);\n \n cleanup:\n-\tkh_destroy_str(shown);\n+\tstrset_clear(&shown);\n \tstrbuf_release(&response_token);\n \tstrbuf_release(&requested_token_id);\n \tstrbuf_release(&payload);\n-- \ngitgitgadget\n"},{"id":"541682","messageId":"487628C4-596C-4870-A652-E1670C700AD7@gmail.com","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-04-15T17:50:19Z","receivedAt":"2026-04-15T17:50:32Z","isPatch":true,"body":"\n> Le 15 avr. 2026 à 09:27, Paul Tarjan via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n> \n> ﻿This series implements the built-in fsmonitor daemon for Linux using the\n> inotify API, bringing it to feature parity with the existing Windows and\n> macOS implementations. It also fixes two memory leaks in the\n> platform-independent daemon code and deduplicates the IPC and settings logic\n> that is now shared between macOS and Linux.\n\nTroubleshooting a Gentoo build failure of next has me pretty convinced this topic is in there already. Junio should probably check my math, but I think that means we want to see fixes on top of that base now (unless we are reverting this topic from next and queuing a new version?).\n\n(The failure is a Gentoo-ism; we carry a patch that stops applying with this series. Not anything this project needs to worry about.)"},{"id":"541687","messageId":"xmqqeckg6qso.fsf@gitster.g","threadId":"64696","inReplyTo":"pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com","subject":"Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-15T18:07:19Z","receivedAt":"2026-04-15T18:07:22Z","isPatch":true,"body":"\"Paul Tarjan via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Changes since v14:\n>\n>  * Fix stop_daemon_delete_repo: test_might_fail doesn't allow maybe_timeout,\n>    replaced with { ... || :; } per SZEDER/Peff review, restoring broken\n>    &&-chain\n\nI verified that this is the only change since the version this\niteration replaces.  Will queue.\n\nThanks.\n\n\n"},{"id":"541690","messageId":"xmqq5x5s6p3w.fsf@gitster.g","threadId":"64696","inReplyTo":"487628C4-596C-4870-A652-E1670C700AD7@gmail.com","subject":"Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-15T18:43:47Z","receivedAt":"2026-04-15T18:43:50Z","isPatch":true,"body":"Ben Knoble <ben.knoble@gmail.com> writes:\n\n>> Le 15 avr. 2026 à 09:27, Paul Tarjan via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n>> \n>> ﻿This series implements the built-in fsmonitor daemon for Linux using the\n>> inotify API, bringing it to feature parity with the existing Windows and\n>> macOS implementations. It also fixes two memory leaks in the\n>> platform-independent daemon code and deduplicates the IPC and settings logic\n>> that is now shared between macOS and Linux.\n>\n> Troubleshooting a Gentoo build failure of next has me pretty\n> convinced this topic is in there already. Junio should probably\n> check my math, but I think that means we want to see fixes on top\n> of that base now (unless we are reverting this topic from next and\n> queuing a new version?).\n>\n> (The failure is a Gentoo-ism; we carry a patch that stops applying\n> with this series. Not anything this project needs to worry about.)\n\nI reverted it yesterday out of 'next'.  I do not recall I had a\nchance to push it out, but my intention is keep this round in 'seen'\nbut out of 'next' until people are happy with it (which means folks\nwho test diligently like you do would need to peek into 'seen' as\nwell as 'next').\n"},{"id":"541698","messageId":"57A4EEF1-6D80-4CCB-9D42-EB919A3A7692@gmail.com","threadId":"64696","inReplyTo":"xmqq5x5s6p3w.fsf@gitster.g","subject":"Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-04-15T21:17:55Z","receivedAt":"2026-04-15T21:18:07Z","isPatch":true,"body":"\n> \n> Le 15 avr. 2026 à 14:43, Junio C Hamano <gitster@pobox.com> a écrit :\n> \n> ﻿Ben Knoble <ben.knoble@gmail.com> writes:\n> \n>>>> Le 15 avr. 2026 à 09:27, Paul Tarjan via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n>>> \n>>> ﻿This series implements the built-in fsmonitor daemon for Linux using the\n>>> inotify API, bringing it to feature parity with the existing Windows and\n>>> macOS implementations. It also fixes two memory leaks in the\n>>> platform-independent daemon code and deduplicates the IPC and settings logic\n>>> that is now shared between macOS and Linux.\n>> \n>> Troubleshooting a Gentoo build failure of next has me pretty\n>> convinced this topic is in there already. Junio should probably\n>> check my math, but I think that means we want to see fixes on top\n>> of that base now (unless we are reverting this topic from next and\n>> queuing a new version?).\n>> \n>> (The failure is a Gentoo-ism; we carry a patch that stops applying\n>> with this series. Not anything this project needs to worry about.)\n> \n> I reverted it yesterday out of 'next'.  I do not recall I had a\n> chance to push it out, but my intention is keep this round in 'seen'\n> but out of 'next' until people are happy with it (which means folks\n> who test diligently like you do would need to peek into 'seen' as\n> well as 'next').\n\nThanks. Hopefully I’ll get to both seen and next soon. "},{"id":"543142","messageId":"xmqqa4u5nnxq.fsf@gitster.g","threadId":"64696","inReplyTo":"487628C4-596C-4870-A652-E1670C700AD7@gmail.com","subject":"Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-12T06:26:25Z","receivedAt":"2026-05-12T06:26:29Z","isPatch":true,"body":"Ben Knoble <ben.knoble@gmail.com> writes:\n\n>> Le 15 avr. 2026 à 09:27, Paul Tarjan via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n>> \n>> ﻿This series implements the built-in fsmonitor daemon for Linux using the\n>> inotify API, bringing it to feature parity with the existing Windows and\n>> macOS implementations. It also fixes two memory leaks in the\n>> platform-independent daemon code and deduplicates the IPC and settings logic\n>> that is now shared between macOS and Linux.\n>\n> Troubleshooting a Gentoo build failure of next has me pretty\n> convinced this topic is in there already. Junio should probably\n> check my math, but I think that means we want to see fixes on top\n> of that base now (unless we are reverting this topic from next and\n> queuing a new version?).\n>\n> (The failure is a Gentoo-ism; we carry a patch that stops applying\n> with this series. Not anything this project needs to worry about.)\n\nSo is there a verdict already, which this project may not need to\nworry about?  This has been kept out of 'next' after getting\nreverted but if the breakage was due to Gentoo-ism whose workaround\ndoes not need to get upstreamed, and if there are no other reasons to\nblock the topic, I am inclined to mark the topic for 'next'.\n\nThanks.\n"},{"id":"543216","messageId":"CALnO6CBOY6xk_sWeqtwWJXp9UPbY0Opd8Af1xk-j3QbdAtbOOw@mail.gmail.com","threadId":"64696","inReplyTo":"xmqqa4u5nnxq.fsf@gitster.g","subject":"Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-05-12T20:36:36Z","receivedAt":"2026-05-12T20:36:49Z","isPatch":true,"body":"On Tue, May 12, 2026 at 2:26 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Ben Knoble <ben.knoble@gmail.com> writes:\n>\n> >> Le 15 avr. 2026 à 09:27, Paul Tarjan via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n> >>\n> >> ﻿This series implements the built-in fsmonitor daemon for Linux using the\n> >> inotify API, bringing it to feature parity with the existing Windows and\n> >> macOS implementations. It also fixes two memory leaks in the\n> >> platform-independent daemon code and deduplicates the IPC and settings logic\n> >> that is now shared between macOS and Linux.\n> >\n> > Troubleshooting a Gentoo build failure of next has me pretty\n> > convinced this topic is in there already. Junio should probably\n> > check my math, but I think that means we want to see fixes on top\n> > of that base now (unless we are reverting this topic from next and\n> > queuing a new version?).\n> >\n> > (The failure is a Gentoo-ism; we carry a patch that stops applying\n> > with this series. Not anything this project needs to worry about.)\n>\n> So is there a verdict already, which this project may not need to\n> worry about?  This has been kept out of 'next' after getting\n> reverted but if the breakage was due to Gentoo-ism whose workaround\n> does not need to get upstreamed, and if there are no other reasons to\n> block the topic, I am inclined to mark the topic for 'next'.\n>\n> Thanks.\n\nI think Gentoo folks know what needs done (which I will try to spend\nsome time on soon) to adjust the patches we carry. (That will not need\nto get upstreamed, no.)\n\nI would certainly not keep this out of the tree only on behalf of\nGentoo, unless there are other issues.\n\n-- \nD. Ben Knoble\n"}]}