1use crate::Result;
2use notify::{recommended_watcher, Event, RecommendedWatcher, RecursiveMode, Watcher};
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5use std::sync::mpsc::{channel, Receiver};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum GitEvent {
10 IndexChanged,
11 HeadChanged,
12 RefsChanged,
13 ConfigChanged,
14 WorktreeChanged,
15}
16
17pub struct GitWatcher {
18 _watcher: RecommendedWatcher,
19 receiver: Receiver<notify::Result<Event>>,
20 git_dir: PathBuf,
21}
22
23impl GitWatcher {
24 pub fn new(git_dir: impl AsRef<Path>) -> Result<Self> {
25 let git_dir = git_dir.as_ref().to_path_buf();
26 let (tx, rx) = channel();
27
28 let mut watcher = recommended_watcher(tx)?;
29 watcher.watch(&git_dir, RecursiveMode::Recursive)?;
30
31 Ok(Self {
32 _watcher: watcher,
33 receiver: rx,
34 git_dir,
35 })
36 }
37
38 pub fn try_recv(&self) -> Option<GitEvent> {
39 self.receiver
40 .try_recv()
41 .ok()
42 .and_then(|r| r.ok())
43 .and_then(|e| self.classify_event(&e))
44 }
45
46 pub fn recv_timeout(&self, timeout: Duration) -> Option<GitEvent> {
47 self.receiver
48 .recv_timeout(timeout)
49 .ok()
50 .and_then(|r| r.ok())
51 .and_then(|e| self.classify_event(&e))
52 }
53
54 fn classify_event(&self, event: &Event) -> Option<GitEvent> {
55 for path in &event.paths {
56 if let Ok(relative) = path.strip_prefix(&self.git_dir) {
57 let rel_str = relative.to_string_lossy();
58
59 if rel_str == "index" || rel_str.starts_with("index.") {
60 return Some(GitEvent::IndexChanged);
61 }
62 if rel_str == "HEAD" {
63 return Some(GitEvent::HeadChanged);
64 }
65 if rel_str.starts_with("refs/") {
66 return Some(GitEvent::RefsChanged);
67 }
68 if rel_str == "config" {
69 return Some(GitEvent::ConfigChanged);
70 }
71 }
72 }
73
74 if event.paths.iter().any(|p| !p.starts_with(&self.git_dir)) {
75 return Some(GitEvent::WorktreeChanged);
76 }
77
78 None
79 }
80}