use super::events::{FileEvent, IndexingEvent, WorktreeId};
use super::ignore::IgnorePatternMatcher;
use super::watcher::{FileWatcher, WatcherConfig};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tokio::sync::mpsc;
use tracing::{debug, error, warn};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatcherStatus {
Running,
Stopped,
Failed(String),
}
pub struct WorktreeWatcher {
worktree_id: WorktreeId,
path: PathBuf,
file_watcher: FileWatcher,
status: WatcherStatus,
}
impl WorktreeWatcher {
pub fn new(
worktree_id: WorktreeId,
path: PathBuf,
config: WatcherConfig,
) -> Result<(Self, mpsc::Receiver<IndexingEvent>)> {
let (file_watcher, file_event_rx) =
FileWatcher::new(path.clone(), config).context("Failed to create FileWatcher")?;
let (indexing_event_tx, indexing_event_rx) = mpsc::channel(1000);
let worktree_id_clone = worktree_id.clone();
let repo_root = path.clone();
tokio::spawn(async move {
Self::event_conversion_task(
worktree_id_clone,
file_event_rx,
indexing_event_tx,
repo_root,
)
.await;
});
let watcher = Self {
worktree_id,
path,
file_watcher,
status: WatcherStatus::Stopped,
};
Ok((watcher, indexing_event_rx))
}
pub fn start(&mut self) -> Result<()> {
self.file_watcher
.watch(&self.path)
.with_context(|| format!("Failed to start watching worktree: {}", self.worktree_id))?;
self.status = WatcherStatus::Running;
debug!(
"Started worktree watcher: {} at path: {}",
self.worktree_id,
self.path.display()
);
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
self.file_watcher
.stop()
.with_context(|| format!("Failed to stop watching worktree: {}", self.worktree_id))?;
self.status = WatcherStatus::Stopped;
debug!("Stopped worktree watcher: {}", self.worktree_id);
Ok(())
}
pub fn restart(&mut self) -> Result<()> {
debug!("Restarting worktree watcher: {}", self.worktree_id);
self.stop()
.context("Failed to stop watcher during restart")?;
self.start()
.context("Failed to start watcher during restart")?;
Ok(())
}
pub fn status(&self) -> &WatcherStatus {
&self.status
}
pub fn worktree_id(&self) -> &WorktreeId {
&self.worktree_id
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn mark_failed(&mut self, error: String) {
warn!("Watcher {} marked as failed: {}", self.worktree_id, error);
self.status = WatcherStatus::Failed(error);
}
async fn event_conversion_task(
worktree_id: WorktreeId,
mut file_event_rx: mpsc::Receiver<FileEvent>,
indexing_event_tx: mpsc::Sender<IndexingEvent>,
repo_root: PathBuf,
) {
let ignore_matcher = match IgnorePatternMatcher::from_repository(&repo_root) {
Ok(matcher) => matcher,
Err(e) => {
error!(
"Failed to load ignore patterns, watcher cannot start: {}",
e
);
return; }
};
while let Some(file_event) = file_event_rx.recv().await {
let path = file_event.path();
if ignore_matcher.should_ignore(path) {
debug!("Ignoring event for maproomignore path: {}", path.display());
continue;
}
let timestamp = SystemTime::now();
let indexing_event =
IndexingEvent::from_file_event(worktree_id.clone(), file_event, timestamp);
if let Err(e) = indexing_event_tx.send(indexing_event).await {
warn!(
"Failed to send indexing event for worktree {}: {}",
worktree_id, e
);
return;
}
}
debug!(
"Event conversion task exiting for worktree: {}",
worktree_id
);
}
}
impl Drop for WorktreeWatcher {
fn drop(&mut self) {
let _ = self.stop();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watcher_status() {
assert_eq!(WatcherStatus::Running, WatcherStatus::Running);
assert_eq!(WatcherStatus::Stopped, WatcherStatus::Stopped);
assert_ne!(WatcherStatus::Running, WatcherStatus::Stopped);
let failed = WatcherStatus::Failed("error".to_string());
assert!(matches!(failed, WatcherStatus::Failed(_)));
}
#[test]
fn test_worktree_id_accessor() {
let worktree_id = "test-worktree".to_string();
assert_eq!(worktree_id, "test-worktree");
}
}