1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! What:
//! The fsmonitor section of `git cli-git doctor`: whether Git's own file-system monitor (`core.fsmonitor`) is off, and
//! where it is on, the file and scope that turn it on and the command that turns it off.
//! Why:
//! The owner decided on 2026-11-03: "If git's fsmonitor is fundamentally broken and cannot be easily or meaningfully
//! worked around, disable git's fsmonitor globally entirely and make doctor say so." It is off in the person's global
//! configuration, and the wrapper never passes `-c core.fsmonitor=...` to its children, so doctor is where the state is
//! visible: one line when it is off, and a finding with the exact command when some scope turns it on.
//! Doctor reports and never writes configuration. The exit status does not depend on it, like the notes about retired keys.
//! The origin of a value (the file) is not in the configuration snapshot the commit path reads, which lists scopes only,
//! so this section reads one more listing with `--show-origin`; doctor runs on request, never on the commit path.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const text = renderFsmonitor(parseFsmonitorEntries(await git(['config', '--list', '--show-origin', '--show-scope', '-z'])));
//! ```
use super::config_snapshot::canonical_bool;
use super::diagnostic_log::debug;
use super::transaction_git::{GitContext, GitRequest, run_git};
use std::path::Path;
/// The key this section reads, as `git config --list` prints it (section and variable in lower case).
const FSMONITOR_KEY: &str = "core.fsmonitor";
/// The scopes that `git config` can write with a flag of the same name (`--system`, `--global`, `--local`,
/// `--worktree`); the `command` scope (`-c` and the environment) has no file.
const WRITABLE_SCOPES: [&str; 4] = ["system", "global", "local", "worktree"];
/// The repository-relative path of the document that records why the owner turned Git's fsmonitor off.
pub const FSMONITOR_DOCUMENT: &str = "doc/troubleshooting/git-fsmonitor-daemon-start-wait.md";
/// The date of the owner's decision to keep Git's fsmonitor off.
pub const DECISION_DATE: &str = "2026-11-03";
/// The one-sentence reason, from measurements of 2026-11-03 on the owner's main checkout (261 daemons held 4,591,335
/// watches; one daemon watched 2,269,377 directories and spent 117.7 s of CPU and 734 MB to start; a running daemon
/// saved about 7 ms per `git status`) and from Git 2.56.0 (`compat/fsmonitor/fsm-listen-linux.c`,
/// `builtin/fsmonitor--daemon.c`); the full account is in the document named by `FSMONITOR_DOCUMENT`.
pub const FSMONITOR_REASON: &str = "Git's fsmonitor daemon registers a watch on every directory of the work tree, \
linked worktrees inside it included, before it answers, so a Git command that finds no running daemon starts one and \
waits for it for up to `fsmonitor.startTimeout` (60 s by default), while a running daemon saved about 7 ms per \
`git status` on the owner's main checkout.";
/// One `core.fsmonitor` entry of the configuration listing.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FsmonitorEntry {
/// `system`, `global`, `local`, `worktree` or `command`.
pub scope: String,
/// Where Git read the entry, as it prints it: `file:<path>` or `command line:`.
pub origin: String,
/// The value bytes, or `None` for a key written without `=`.
pub value: Option<Vec<u8>>,
}
/// What:
/// The `core.fsmonitor` entries of a `git config --list --show-origin --show-scope -z` listing, in precedence order.
/// `Option<Vec<...>>` is `None` when the listing is not in the expected shape.
/// Why:
/// The listing is `scope NUL origin NUL key [LF value] NUL` repeated, and the last entry of a key is the one Git
/// uses; a listing in any other shape is refused whole, so a half-understood configuration is never reported on.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseFsmonitorEntries(bytes); // undefined when the shape is wrong
/// ```
pub fn parse_fsmonitor_entries(listing: &[u8]) -> Option<Vec<FsmonitorEntry>> {
let mut entries: Vec<FsmonitorEntry> = Vec::new();
if listing.is_empty() {
return Some(entries);
}
let body: &[u8] = listing.strip_suffix(&[0])?;
let mut fields = body.split(|byte| { return *byte == 0; });
while let Some(scope) = fields.next() {
let origin: &[u8] = fields.next()?;
let record: &[u8] = fields.next()?;
// Only the first line feed separates key from value; the value may hold more of them.
let (key, value): (&[u8], Option<&[u8]>) = match record.iter().position(|byte| { return *byte == b'\n'; }) {
Some(newline) => (&record[..newline], Some(&record[newline + 1..])),
None => (record, None),
};
if key != FSMONITOR_KEY.as_bytes() {
continue;
}
entries.push(FsmonitorEntry {
scope: String::from(std::str::from_utf8(scope).ok()?),
origin: String::from(std::str::from_utf8(origin).ok()?),
value: value.map(<[u8]>::to_vec),
});
}
return Some(entries);
}
/// What:
/// The command that turns `core.fsmonitor` off for the scope of an entry, or the instruction to remove a value that
/// no file holds.
/// Why:
/// `git config --<scope> core.fsmonitor false` rewrites the file of that scope; a `command`-scope value comes from `-c`
/// or the environment, which no configuration command can change. When the entry came from a file the scope includes,
/// the command changes the including file and doctor then names the included file, which a second run shows.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// WRITABLE_SCOPES.includes(scope) ? `Turn it off with: git config --${scope} core.fsmonitor false` : '...';
/// ```
fn remedy(entry: &FsmonitorEntry) -> String {
if WRITABLE_SCOPES.contains(&entry.scope.as_str()) {
return format!("Turn it off with: git config --{} core.fsmonitor false", entry.scope);
}
return String::from(
"No configuration file sets it: remove the `-c core.fsmonitor=...` option or the GIT_CONFIG_COUNT or \
GIT_CONFIG_PARAMETERS environment variable that passes it.",
);
}
/// What:
/// The standard error text for the entries of `core.fsmonitor`: the off line, or the finding.
/// Why:
/// The last entry decides, as for `git config --get`. Off (`false`, `no`, `off`, `0`, empty, or no entry) gets one line
/// that names the entry that decided; on (a true word, a non-zero number, a key without a value, or a hook path) gets a
/// finding with the file, the scope, the reason, the document and the command for that scope.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// renderFsmonitor(entries); // one line ending in a line feed
/// ```
pub fn render_fsmonitor(entries: &[FsmonitorEntry]) -> String {
let Some(deciding) = entries.last() else {
return format!(
"git cli-git doctor: Git's fsmonitor is off by the owner's decision of {DECISION_DATE} (core.fsmonitor is not set). \
{FSMONITOR_REASON} See {FSMONITOR_DOCUMENT}.\n"
);
};
let place: String = format!("{}, {} scope", deciding.origin, deciding.scope);
let state: Option<bool> = canonical_bool(deciding.value.as_deref());
if state == Some(false) {
// The value is named as written; a spelling other than `false` also says how Git reads it.
let written: String = String::from_utf8_lossy(deciding.value.as_deref().unwrap_or_default()).into_owned();
let value: String = if written == "false" {
String::from("false")
} else if written.is_empty() {
String::from("empty (Git reads it as false)")
} else {
format!("{written} (Git reads it as false)")
};
return format!(
"git cli-git doctor: Git's fsmonitor is off by the owner's decision of {DECISION_DATE} (core.fsmonitor is {value} in \
{place}). {FSMONITOR_REASON} See {FSMONITOR_DOCUMENT}.\n"
);
}
// A value that is not a boolean word or a number is the path of a hook program, which also turns fsmonitor on.
let setting: String = match (state, deciding.value.as_deref()) {
(Some(_), None) => format!("core.fsmonitor is true (the key has no value) in {place}"),
(Some(_), Some(value)) => format!("core.fsmonitor is {} in {place}", String::from_utf8_lossy(value)),
(None, Some(value)) => format!("core.fsmonitor names the hook {} in {place}", String::from_utf8_lossy(value)),
(None, None) => unreachable!("a key without a value always reads as true"),
};
return format!(
"git cli-git doctor: Git's fsmonitor is on, against the owner's decision of {DECISION_DATE} to keep it off: {setting}. \
{FSMONITOR_REASON} See {FSMONITOR_DOCUMENT}. {}\n",
remedy(deciding)
);
}
/// What:
/// Read the listing for `cwd` and render the fsmonitor text.
/// Why:
/// Doctor reports what it could not read rather than staying silent, because silence would read as "off"; the exit
/// status is unaffected either way.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await fsmonitorReport({ context, cwd });
/// ```
pub fn fsmonitor_report(context: &GitContext, cwd: &Path) -> String {
let request: GitRequest = GitRequest::new(cwd, &["config", "--list", "--show-origin", "--show-scope", "-z"]);
let output = match run_git(context, &request) {
Ok(finished) => finished,
Err(error) => {
debug("fsmonitorReport", format!("git config --list could not start: {error}").as_str());
return format!("git cli-git doctor: could not read Git's fsmonitor setting: git config --list could not start: {error}\n");
}
};
if !output.succeeded() {
debug("fsmonitorReport", format!("git config --list failed: {}", output.error_text()).as_str());
return format!("git cli-git doctor: could not read Git's fsmonitor setting: git config --list failed: {}\n", output.error_text());
}
let Some(entries) = parse_fsmonitor_entries(output.stdout.as_slice()) else {
debug("fsmonitorReport", "the configuration listing has an unexpected shape");
return String::from("git cli-git doctor: could not read Git's fsmonitor setting: git config --list printed an unexpected shape\n");
};
return render_fsmonitor(entries.as_slice());
}
/// Fsmonitor controls are in the test module.
#[cfg(test)]
#[path = "doctor_fsmonitor_tests.rs"]
mod tests;