gitent_server/
watcher.rs

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        // Just verify it doesn't panic
157    }
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}