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