Line data Source code
1 : use anyhow::Result;
2 : use git2::Repository;
3 : use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher};
4 : use std::path::Path;
5 : use tokio::sync::mpsc;
6 :
7 : use crate::diff::DiffSnapshot;
8 : use crate::git::GitRepo;
9 :
10 : // Debug logging helper
11 4 : fn debug_log(msg: String) {
12 4 : crate::logger::debug(msg);
13 4 : }
14 :
15 : pub struct FileWatcher {
16 : _watcher: RecommendedWatcher,
17 : }
18 :
19 : impl FileWatcher {
20 27 : pub fn new(
21 27 : git_repo: GitRepo,
22 27 : snapshot_sender: mpsc::UnboundedSender<DiffSnapshot>,
23 27 : ) -> Result<Self> {
24 27 : let repo_path = git_repo.repo_path().to_path_buf();
25 :
26 27 : let (tx, rx) = std::sync::mpsc::channel();
27 :
28 27 : let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
29 :
30 27 : watcher.watch(repo_path.as_ref(), RecursiveMode::Recursive)?;
31 :
32 : // Spawn a task to handle file system events
33 27 : tokio::spawn(async move {
34 1 : let mut last_snapshot_time = std::time::Instant::now();
35 1 : let debounce_duration = std::time::Duration::from_millis(500);
36 :
37 1 : debug_log(format!("File watcher started for {:?}", repo_path));
38 :
39 : loop {
40 3 : match rx.recv() {
41 2 : Ok(Ok(event)) => {
42 : // Only process events for git-tracked files
43 2 : if should_process_event(&event, &repo_path) {
44 1 : debug_log(format!("Received event: {:?}", event));
45 1 : debug_log("Processing event for snapshot".to_string());
46 : // Debounce: only create a new snapshot if enough time has passed
47 1 : let now = std::time::Instant::now();
48 1 : if now.duration_since(last_snapshot_time) >= debounce_duration {
49 1 : if let Ok(snapshot) = git_repo.get_diff_snapshot() {
50 1 : debug_log(format!(
51 : "Created snapshot with {} files",
52 1 : snapshot.files.len()
53 : ));
54 : // Only send if there are actual changes
55 1 : if !snapshot.files.is_empty() {
56 1 : let _ = snapshot_sender.send(snapshot);
57 1 : last_snapshot_time = now;
58 1 : } else {
59 0 : debug_log("Snapshot was empty, not sending".to_string());
60 0 : }
61 0 : }
62 0 : } else {
63 0 : debug_log("Debouncing, too soon since last snapshot".to_string());
64 0 : }
65 1 : } else if crate::logger::filtered_events_enabled() {
66 0 : crate::logger::trace(format!("Filtered event: {:?}", event));
67 1 : }
68 : }
69 0 : Ok(Err(e)) => {
70 0 : debug_log(format!("Watch error: {:?}", e));
71 0 : }
72 1 : Err(_) => break,
73 : }
74 : }
75 1 : });
76 :
77 27 : Ok(Self { _watcher: watcher })
78 27 : }
79 : }
80 :
81 9 : fn should_process_event(event: &Event, repo_path: &Path) -> bool {
82 : use notify::EventKind;
83 :
84 : // Filter out events we don't care about
85 9 : match event.kind {
86 : EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) => {
87 : // Check if any of the paths are:
88 : // 1. Not in .git directory (working directory changes), OR
89 : // 2. The .git/index file specifically (staging changes)
90 7 : event.paths.iter().any(|path| {
91 : // Check if it's the git index file
92 7 : if path.ends_with(".git/index") {
93 1 : return true;
94 6 : }
95 :
96 : // Check if it's a working directory file (not in .git)
97 6 : let rel_path = match path.strip_prefix(repo_path) {
98 5 : Ok(p) => p,
99 1 : Err(_) => return false,
100 : };
101 :
102 5 : if rel_path
103 5 : .components()
104 5 : .next()
105 5 : .map(|c| c.as_os_str() == ".git")
106 5 : .unwrap_or(false)
107 : {
108 1 : return false;
109 4 : }
110 :
111 : // Ignore files excluded by gitignore/excludes.
112 4 : !is_git_ignored(repo_path, rel_path)
113 7 : })
114 : }
115 2 : _ => false,
116 : }
117 9 : }
118 :
119 4 : fn is_git_ignored(repo_path: &Path, rel_path: &Path) -> bool {
120 4 : match Repository::open(repo_path) {
121 2 : Ok(repo) => repo.status_should_ignore(rel_path).unwrap_or(false),
122 2 : Err(_) => false,
123 : }
124 4 : }
125 :
126 : #[cfg(test)]
127 : #[path = "../tests/watcher.rs"]
128 : mod tests;
|