LCOV - code coverage report
Current view: top level - src - watcher.rs (source / functions) Coverage Total Hit
Test: Hunky Coverage Lines: 85.7 % 70 60
Test Date: 2026-02-25 04:31:59 Functions: 100.0 % 7 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            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;
        

Generated by: LCOV version 2.0-1