1#![warn(missing_docs)]
24
25use std::path::PathBuf;
26
27use crate::config::ConfigGetError;
28use crate::settings::UserSettings;
29
30#[derive(Eq, PartialEq, Clone, Debug)]
32pub struct WatchmanConfig {
33 pub register_trigger: bool,
35}
36
37#[derive(Eq, PartialEq, Clone, Debug)]
39pub enum FsmonitorSettings {
40 Watchman(WatchmanConfig),
42
43 Test {
45 changed_files: Vec<PathBuf>,
48 },
49
50 None,
55}
56
57impl FsmonitorSettings {
58 pub fn from_settings(settings: &UserSettings) -> Result<FsmonitorSettings, ConfigGetError> {
60 let name = "core.fsmonitor";
61 match settings.get_string(name)?.as_ref() {
62 "watchman" => Ok(Self::Watchman(WatchmanConfig {
63 register_trigger: settings.get_bool("core.watchman.register-snapshot-trigger")?,
64 })),
65 "test" => Err(ConfigGetError::Type {
66 name: name.to_owned(),
67 error: "Cannot use test fsmonitor in real repository".into(),
68 source_path: None,
69 }),
70 "none" => Ok(Self::None),
71 other => Err(ConfigGetError::Type {
72 name: name.to_owned(),
73 error: format!("Unknown fsmonitor kind: {other}").into(),
74 source_path: None,
75 }),
76 }
77 }
78}
79
80#[cfg(feature = "watchman")]
84pub mod watchman {
85 use std::path::Path;
86 use std::path::PathBuf;
87
88 use itertools::Itertools as _;
89 use thiserror::Error;
90 use tracing::info;
91 use tracing::instrument;
92 use watchman_client::expr;
93 use watchman_client::prelude::Clock as InnerClock;
94 use watchman_client::prelude::ClockSpec;
95 use watchman_client::prelude::NameOnly;
96 use watchman_client::prelude::QueryRequestCommon;
97 use watchman_client::prelude::QueryResult;
98 use watchman_client::prelude::TriggerRequest;
99
100 #[derive(Clone, Debug)]
109 pub struct Clock(InnerClock);
110
111 impl From<crate::protos::working_copy::WatchmanClock> for Clock {
112 fn from(clock: crate::protos::working_copy::WatchmanClock) -> Self {
113 use crate::protos::working_copy::watchman_clock::WatchmanClock;
114 let watchman_clock = clock.watchman_clock.unwrap();
115 let clock = match watchman_clock {
116 WatchmanClock::StringClock(string_clock) => {
117 InnerClock::Spec(ClockSpec::StringClock(string_clock))
118 }
119 WatchmanClock::UnixTimestamp(unix_timestamp) => {
120 InnerClock::Spec(ClockSpec::UnixTimestamp(unix_timestamp))
121 }
122 };
123 Self(clock)
124 }
125 }
126
127 impl From<Clock> for crate::protos::working_copy::WatchmanClock {
128 fn from(clock: Clock) -> Self {
129 use crate::protos::working_copy::watchman_clock;
130 use crate::protos::working_copy::WatchmanClock;
131 let Clock(clock) = clock;
132 let watchman_clock = match clock {
133 InnerClock::Spec(ClockSpec::StringClock(string_clock)) => {
134 watchman_clock::WatchmanClock::StringClock(string_clock)
135 }
136 InnerClock::Spec(ClockSpec::UnixTimestamp(unix_timestamp)) => {
137 watchman_clock::WatchmanClock::UnixTimestamp(unix_timestamp)
138 }
139 InnerClock::ScmAware(_) => {
140 unimplemented!("SCM-aware Watchman clocks not supported")
141 }
142 };
143 WatchmanClock {
144 watchman_clock: Some(watchman_clock),
145 }
146 }
147 }
148
149 #[expect(missing_docs)]
150 #[derive(Debug, Error)]
151 pub enum Error {
152 #[error("Could not connect to Watchman")]
153 WatchmanConnectError(#[source] watchman_client::Error),
154
155 #[error("Could not canonicalize working copy root path")]
156 CanonicalizeRootError(#[source] std::io::Error),
157
158 #[error("Watchman failed to resolve the working copy root path")]
159 ResolveRootError(#[source] watchman_client::Error),
160
161 #[error("Failed to query Watchman")]
162 WatchmanQueryError(#[source] watchman_client::Error),
163
164 #[error("Failed to register Watchman trigger")]
165 WatchmanTriggerError(#[source] watchman_client::Error),
166 }
167
168 pub struct Fsmonitor {
170 client: watchman_client::Client,
171 resolved_root: watchman_client::ResolvedRoot,
172 }
173
174 impl Fsmonitor {
175 #[instrument]
180 pub async fn init(
181 working_copy_path: &Path,
182 config: &super::WatchmanConfig,
183 ) -> Result<Self, Error> {
184 info!("Initializing Watchman filesystem monitor...");
185 let connector = watchman_client::Connector::new();
186 let client = connector
187 .connect()
188 .await
189 .map_err(Error::WatchmanConnectError)?;
190 let working_copy_root = watchman_client::CanonicalPath::canonicalize(working_copy_path)
191 .map_err(Error::CanonicalizeRootError)?;
192 let resolved_root = client
193 .resolve_root(working_copy_root)
194 .await
195 .map_err(Error::ResolveRootError)?;
196
197 let monitor = Fsmonitor {
198 client,
199 resolved_root,
200 };
201
202 if !config.register_trigger {
205 monitor.unregister_trigger().await?;
206 } else if !monitor.is_trigger_registered().await? {
207 monitor.register_trigger().await?;
208 }
209 Ok(monitor)
210 }
211
212 #[instrument(skip(self))]
218 pub async fn query_changed_files(
219 &self,
220 previous_clock: Option<Clock>,
221 ) -> Result<(Clock, Option<Vec<PathBuf>>), Error> {
222 info!("Querying Watchman for changed files...");
225 let QueryResult {
226 version: _,
227 is_fresh_instance,
228 files,
229 clock,
230 state_enter: _,
231 state_leave: _,
232 state_metadata: _,
233 saved_state_info: _,
234 debug: _,
235 }: QueryResult<NameOnly> = self
236 .client
237 .query(
238 &self.resolved_root,
239 QueryRequestCommon {
240 since: previous_clock.map(|Clock(clock)| clock),
241 expression: Some(self.build_exclude_expr()),
242 ..Default::default()
243 },
244 )
245 .await
246 .map_err(Error::WatchmanQueryError)?;
247
248 let clock = Clock(clock);
249 if is_fresh_instance {
250 Ok((clock, None))
255 } else {
256 let paths = files
257 .unwrap_or_default()
258 .into_iter()
259 .map(|NameOnly { name }| name.into_inner())
260 .collect_vec();
261 Ok((clock, Some(paths)))
262 }
263 }
264
265 #[instrument(skip(self))]
267 pub async fn is_trigger_registered(&self) -> Result<bool, Error> {
268 info!("Checking for an existing Watchman trigger...");
269 Ok(self
270 .client
271 .list_triggers(&self.resolved_root)
272 .await
273 .map_err(Error::WatchmanTriggerError)?
274 .triggers
275 .iter()
276 .any(|t| t.name == "jj-background-monitor"))
277 }
278
279 #[instrument(skip(self))]
281 async fn register_trigger(&self) -> Result<(), Error> {
282 info!("Registering Watchman trigger...");
283 self.client
284 .register_trigger(
285 &self.resolved_root,
286 TriggerRequest {
287 name: "jj-background-monitor".to_string(),
288 command: vec![
289 "jj".to_string(),
290 "debug".to_string(),
291 "snapshot".to_string(),
292 ],
293 expression: Some(self.build_exclude_expr()),
294 ..Default::default()
295 },
296 )
297 .await
298 .map_err(Error::WatchmanTriggerError)?;
299 Ok(())
300 }
301
302 #[instrument(skip(self))]
304 async fn unregister_trigger(&self) -> Result<(), Error> {
305 info!("Unregistering Watchman trigger...");
306 self.client
307 .remove_trigger(&self.resolved_root, "jj-background-monitor")
308 .await
309 .map_err(Error::WatchmanTriggerError)?;
310 Ok(())
311 }
312
313 fn build_exclude_expr(&self) -> expr::Expr {
315 let exclude_dirs = [Path::new(".git"), Path::new(".jj")];
317 let excludes = itertools::chain(
318 [expr::Expr::Name(expr::NameTerm {
320 paths: exclude_dirs.iter().map(|&name| name.to_owned()).collect(),
321 wholename: true,
322 })],
323 exclude_dirs.iter().map(|&name| {
325 expr::Expr::DirName(expr::DirNameTerm {
326 path: name.to_owned(),
327 depth: None,
328 })
329 }),
330 )
331 .collect();
332 expr::Expr::Not(Box::new(expr::Expr::Any(excludes)))
333 }
334 }
335}