1use gitent_core::{Change, ChangeType, Session, Storage};
2use notify::{Event, EventKind, RecursiveMode, Watcher};
3use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap};
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6use std::time::Duration;
7use tokio::sync::mpsc;
8use tracing::{error, info};
9use uuid::Uuid;
10
11pub struct FileWatcher {
12 _session_id: Uuid,
13 _storage: Arc<Mutex<Storage>>,
14 _debouncer: Debouncer<notify::RecommendedWatcher, FileIdMap>,
15}
16
17impl FileWatcher {
18 pub fn new(session: &Session, storage: Arc<Mutex<Storage>>) -> anyhow::Result<Self> {
19 let session_id = session.id;
20 let root_path = session.root_path.clone();
21 let root_path_for_watch = root_path.clone();
22 let ignore_patterns = session.ignore_patterns.clone();
23 let storage_clone = Arc::clone(&storage);
24
25 let (tx, mut rx) = mpsc::channel(100);
26
27 let debouncer = new_debouncer(
28 Duration::from_millis(500),
29 None,
30 move |result: DebounceEventResult| {
31 if let Err(e) = tx.blocking_send(result) {
32 error!("Failed to send event: {}", e);
33 }
34 },
35 )?;
36
37 let mut watcher = Self {
38 _session_id: session_id,
39 _storage: storage,
40 _debouncer: debouncer,
41 };
42
43 watcher
44 ._debouncer
45 .watcher()
46 .watch(&root_path_for_watch, RecursiveMode::Recursive)?;
47
48 info!("File watcher started for {:?}", root_path);
49
50 tokio::spawn(async move {
51 while let Some(result) = rx.recv().await {
52 match result {
53 Ok(events) => {
54 for event in events {
55 if let Err(e) = Self::handle_event(
56 event.event,
57 session_id,
58 &root_path,
59 &ignore_patterns,
60 &storage_clone,
61 ) {
62 error!("Error handling event: {}", e);
63 }
64 }
65 }
66 Err(errors) => {
67 for error in errors {
68 error!("Watch error: {:?}", error);
69 }
70 }
71 }
72 }
73 });
74
75 Ok(watcher)
76 }
77
78 fn handle_event(
79 event: Event,
80 session_id: Uuid,
81 root_path: &Path,
82 ignore_patterns: &[String],
83 storage: &Arc<Mutex<Storage>>,
84 ) -> anyhow::Result<()> {
85 for path in event.paths {
86 if Self::should_ignore(&path, root_path, ignore_patterns) {
87 continue;
88 }
89
90 let change = match event.kind {
91 EventKind::Create(_) => {
92 info!("File created: {:?}", path);
93 let content = std::fs::read(&path).ok();
94 let mut change = Change::new(ChangeType::Create, path.clone(), session_id);
95 if let Some(content) = content {
96 change = change.with_content_after(content);
97 }
98 Some(change)
99 }
100 EventKind::Modify(_) => {
101 info!("File modified: {:?}", path);
102 let content_after = std::fs::read(&path).ok();
103 let mut change = Change::new(ChangeType::Modify, path.clone(), session_id);
104 if let Some(content) = content_after {
105 change = change.with_content_after(content);
106 }
107 Some(change)
108 }
109 EventKind::Remove(_) => {
110 info!("File removed: {:?}", path);
111 Some(Change::new(ChangeType::Delete, path.clone(), session_id))
112 }
113 _ => None,
114 };
115
116 if let Some(change) = change {
117 let storage = storage.lock().unwrap();
118 storage.create_change(&change)?;
119 }
120 }
121
122 Ok(())
123 }
124
125 fn should_ignore(path: &Path, root_path: &Path, ignore_patterns: &[String]) -> bool {
126 let relative_path = path.strip_prefix(root_path).unwrap_or(path);
127 let path_str = relative_path.to_string_lossy();
128
129 for pattern in ignore_patterns {
130 if path_str.contains(pattern) {
131 return true;
132 }
133 }
134
135 false
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use gitent_core::Session;
143 use std::path::PathBuf;
144 use tempfile::TempDir;
145
146 #[tokio::test]
147 async fn test_file_watcher_creation() {
148 let temp_dir = TempDir::new().unwrap();
149 let session = Session::new(temp_dir.path().to_path_buf());
150 let storage = Arc::new(Mutex::new(Storage::in_memory().unwrap()));
151
152 storage.lock().unwrap().create_session(&session).unwrap();
153
154 let _watcher = FileWatcher::new(&session, storage).unwrap();
155
156 }
158
159 #[test]
160 fn test_should_ignore() {
161 let root = PathBuf::from("/test");
162 let ignore_patterns = vec!["target".to_string(), ".git".to_string()];
163
164 assert!(FileWatcher::should_ignore(
165 &PathBuf::from("/test/target/debug"),
166 &root,
167 &ignore_patterns
168 ));
169
170 assert!(FileWatcher::should_ignore(
171 &PathBuf::from("/test/.git/config"),
172 &root,
173 &ignore_patterns
174 ));
175
176 assert!(!FileWatcher::should_ignore(
177 &PathBuf::from("/test/src/main.rs"),
178 &root,
179 &ignore_patterns
180 ));
181 }
182}