use crate::config_snapshot::canonical_bool;
use crate::diagnostic_log::debug;
use crate::transaction_git::{GitContext, GitRequest, run_git};
use std::path::Path;
const FSMONITOR_KEY: &str = "core.fsmonitor";
const WRITABLE_SCOPES: [&str; 4] = ["system", "global", "local", "worktree"];
pub const FSMONITOR_DOCUMENT: &str = "doc/troubleshooting/git-fsmonitor-daemon-start-wait.md";
pub const DECISION_DATE: &str = "2026-11-03";
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.";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FsmonitorEntry {
pub scope: String,
pub origin: String,
pub value: Option<Vec<u8>>,
}
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()?;
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);
}
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.",
);
}
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) {
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"
);
}
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)
);
}
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());
}
#[cfg(test)]
#[path = "doctor_fsmonitor_tests.rs"]
mod tests;