gitkraft_core/features/repo/
watcher.rs1use std::path::PathBuf;
13use std::sync::mpsc;
14use std::thread::{self, JoinHandle};
15use std::time::Duration;
16
17pub fn spawn_git_watcher<F>(git_dir: PathBuf, on_change: F) -> JoinHandle<()>
24where
25 F: Fn() -> bool + Send + 'static,
26{
27 spawn_git_watcher_with_fallback(git_dir, Duration::from_secs(60), on_change)
28}
29
30pub(crate) fn spawn_git_watcher_with_fallback<F>(
34 git_dir: PathBuf,
35 fallback: Duration,
36 on_change: F,
37) -> JoinHandle<()>
38where
39 F: Fn() -> bool + Send + 'static,
40{
41 thread::spawn(move || {
42 use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
43
44 let (raw_tx, raw_rx) = mpsc::channel::<notify::Result<notify::Event>>();
45
46 let mut watcher = RecommendedWatcher::new(
47 move |res| {
48 let _ = raw_tx.send(res);
49 },
50 Config::default(),
51 )
52 .ok();
53
54 if let Some(ref mut w) = watcher {
55 let _ = w.watch(&git_dir, RecursiveMode::NonRecursive);
61
62 let refs_dir = git_dir.join("refs");
65 if refs_dir.exists() {
66 let _ = w.watch(&refs_dir, RecursiveMode::Recursive);
67 }
68 }
69
70 loop {
71 let _ = raw_rx.recv_timeout(fallback);
73 while raw_rx.try_recv().is_ok() {}
75 thread::sleep(Duration::from_millis(300));
77 while raw_rx.try_recv().is_ok() {}
78
79 if !on_change() {
81 break;
82 }
83 }
84 })
85}
86
87#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::sync::{Arc, Mutex};
93
94 fn wait_for<F: Fn() -> bool>(condition: F, timeout: Duration) -> bool {
95 let deadline = std::time::Instant::now() + timeout;
96 while std::time::Instant::now() < deadline {
97 if condition() {
98 return true;
99 }
100 thread::sleep(Duration::from_millis(50));
101 }
102 false
103 }
104
105 #[test]
106 fn watcher_calls_callback_when_git_head_changes() {
107 let dir = tempfile::tempdir().unwrap();
108 let git_dir = dir.path().join(".git");
109 std::fs::create_dir_all(&git_dir).unwrap();
110 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
112
113 let fired = Arc::new(Mutex::new(false));
114 let fired_clone = Arc::clone(&fired);
115
116 let _handle = spawn_git_watcher(git_dir.clone(), move || {
117 *fired_clone.lock().unwrap() = true;
118 false });
120
121 thread::sleep(Duration::from_millis(200));
123
124 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/feature\n").unwrap();
126
127 assert!(
129 wait_for(|| *fired.lock().unwrap(), Duration::from_secs(2)),
130 "watcher did not call on_change within 2 seconds after HEAD changed"
131 );
132 }
133
134 #[test]
135 fn watcher_fires_fallback_poll_when_no_events() {
136 let dir = tempfile::tempdir().unwrap();
140 let fake_git = dir.path().join(".git_nonexistent");
141
142 let fired = Arc::new(Mutex::new(false));
143 let fired_clone = Arc::clone(&fired);
144
145 let _handle =
146 spawn_git_watcher_with_fallback(fake_git, Duration::from_secs(2), move || {
147 *fired_clone.lock().unwrap() = true;
148 false
149 });
150
151 assert!(
153 wait_for(|| *fired.lock().unwrap(), Duration::from_secs(4)),
154 "fallback poll did not fire within 4 seconds"
155 );
156 }
157
158 #[test]
159 fn watcher_thread_exits_when_callback_returns_false() {
160 let dir = tempfile::tempdir().unwrap();
161 let git_dir = dir.path().join(".git");
162 std::fs::create_dir_all(&git_dir).unwrap();
163
164 let handle =
166 spawn_git_watcher_with_fallback(git_dir.clone(), Duration::from_secs(2), move || {
167 false });
169
170 assert!(
172 wait_for(|| handle.is_finished(), Duration::from_secs(4)),
173 "watcher thread did not exit after callback returned false"
174 );
175 }
176}