use notify_debouncer_full::{
DebounceEventResult, Debouncer, NoCache, new_debouncer_opt,
notify::{Config, RecommendedWatcher, RecursiveMode},
};
use std::collections::HashSet;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
use crate::event::Event;
pub(crate) struct RepoWatcher {
_debouncer: Debouncer<RecommendedWatcher, NoCache>,
}
impl RepoWatcher {
pub fn new(
repo_paths: &[PathBuf],
debounce_ms: u64,
event_tx: UnboundedSender<Event>,
watch_exclude_dirs: &[String],
) -> color_eyre::Result<Self> {
let owned_paths: Vec<PathBuf> = repo_paths.to_vec();
let (bridge_tx, mut bridge_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<PathBuf>>();
let paths_for_routing = owned_paths.clone();
let exclude_set: HashSet<String> = watch_exclude_dirs.iter().cloned().collect();
tokio::spawn(async move {
while let Some(changed_paths) = bridge_rx.recv().await {
let mut affected_repos: HashSet<PathBuf> = HashSet::new();
for changed_path in &changed_paths {
if changed_path
.components()
.any(|c| exclude_set.contains(c.as_os_str().to_string_lossy().as_ref()))
{
continue;
}
if changed_path.components().any(|c| c.as_os_str() == ".git") {
let name = changed_path
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default();
let path_str = changed_path.to_string_lossy();
let is_meaningful = name == "HEAD"
|| name == "index"
|| name == "MERGE_HEAD"
|| name == "REBASE_HEAD"
|| name == "COMMIT_EDITMSG"
|| name == "packed-refs"
|| path_str.contains(".git/refs/");
if !is_meaningful {
continue;
}
}
for repo_path in &paths_for_routing {
if changed_path.starts_with(repo_path) {
affected_repos.insert(repo_path.clone());
break;
}
}
}
for path in affected_repos {
let _ = event_tx.send(Event::RepoChanged(path));
}
}
});
let config = Config::default().with_poll_interval(Duration::from_secs(2));
let mut debouncer = new_debouncer_opt::<_, RecommendedWatcher, NoCache>(
Duration::from_millis(debounce_ms),
None,
move |result: DebounceEventResult| {
if let Ok(events) = result {
let paths: Vec<PathBuf> =
events.into_iter().flat_map(|e| e.event.paths).collect();
if !paths.is_empty() {
let _ = bridge_tx.send(paths);
}
}
},
NoCache,
config,
)?;
for path in &owned_paths {
if path.exists()
&& let Err(e) = debouncer.watch(path, RecursiveMode::Recursive)
{
tracing::warn!("Failed to watch {}: {}", path.display(), e);
}
}
Ok(Self {
_debouncer: debouncer,
})
}
}