LCOV - code coverage report
Current view: top level - src - watcher.rs (source / functions) Coverage Total Hit
Test: Hunky Coverage Lines: 88.7 % 71 63
Test Date: 2026-10-10 16:52:03 Functions: 50.0 % 14 7

            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;
        

Generated by: LCOV version 2.0-1