use std::path::PathBuf;
use std::sync::mpsc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
pub fn spawn_git_watcher<F>(git_dir: PathBuf, on_change: F) -> JoinHandle<()>
where
F: Fn() -> bool + Send + 'static,
{
spawn_git_watcher_with_fallback(git_dir, Duration::from_secs(60), on_change)
}
pub fn spawn_git_watcher_with_fallback<F>(
git_dir: PathBuf,
fallback: Duration,
on_change: F,
) -> JoinHandle<()>
where
F: Fn() -> bool + Send + 'static,
{
thread::spawn(move || {
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
let (raw_tx, raw_rx) = mpsc::channel::<notify::Result<notify::Event>>();
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = raw_tx.send(res);
},
Config::default(),
)
.ok();
if let Some(ref mut w) = watcher {
let _ = w.watch(&git_dir, RecursiveMode::NonRecursive);
let refs_dir = git_dir.join("refs");
if refs_dir.exists() {
let _ = w.watch(&refs_dir, RecursiveMode::Recursive);
}
}
loop {
let _ = raw_rx.recv_timeout(fallback);
while raw_rx.try_recv().is_ok() {}
thread::sleep(Duration::from_millis(300));
while raw_rx.try_recv().is_ok() {}
if !on_change() {
break;
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
fn wait_for<F: Fn() -> bool>(condition: F, timeout: Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if condition() {
return true;
}
thread::sleep(Duration::from_millis(50));
}
false
}
#[test]
fn watcher_calls_callback_when_git_head_changes() {
let dir = tempfile::tempdir().unwrap();
let git_dir = dir.path().join(".git");
std::fs::create_dir_all(&git_dir).unwrap();
std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let fired = Arc::new(Mutex::new(false));
let fired_clone = Arc::clone(&fired);
let _handle = spawn_git_watcher(git_dir.clone(), move || {
*fired_clone.lock().unwrap() = true;
false });
thread::sleep(Duration::from_millis(200));
std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/feature\n").unwrap();
assert!(
wait_for(|| *fired.lock().unwrap(), Duration::from_secs(2)),
"watcher did not call on_change within 2 seconds after HEAD changed"
);
}
#[test]
fn watcher_fires_fallback_poll_when_no_events() {
let dir = tempfile::tempdir().unwrap();
let fake_git = dir.path().join(".git_nonexistent");
let fired = Arc::new(Mutex::new(false));
let fired_clone = Arc::clone(&fired);
let _handle =
spawn_git_watcher_with_fallback(fake_git, Duration::from_secs(2), move || {
*fired_clone.lock().unwrap() = true;
false
});
assert!(
wait_for(|| *fired.lock().unwrap(), Duration::from_secs(4)),
"fallback poll did not fire within 4 seconds"
);
}
#[test]
fn watcher_thread_exits_when_callback_returns_false() {
let dir = tempfile::tempdir().unwrap();
let git_dir = dir.path().join(".git");
std::fs::create_dir_all(&git_dir).unwrap();
let handle =
spawn_git_watcher_with_fallback(git_dir.clone(), Duration::from_secs(2), move || {
false });
assert!(
wait_for(|| handle.is_finished(), Duration::from_secs(4)),
"watcher thread did not exit after callback returned false"
);
}
}