codebase-graph 1.1.5

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::helpers::watch_matches_any_pattern;
use crate::cli::{
    build::{
        default_excluded_parts, read_codebase_graph_ignore, read_materialization_config_rules,
        MaterializeOptions,
    },
    setup::GraphStatePaths,
};
use notify::{
    event::{AccessKind, AccessMode},
    Event, EventKind,
};
use std::{
    collections::BTreeSet,
    env,
    path::{Path, PathBuf},
};

#[derive(Debug)]
pub(in crate::cli) struct WatchEventFilter {
    pub(in crate::cli) source_root: PathBuf,
    pub(in crate::cli) current_dir: PathBuf,
    pub(in crate::cli) excluded_parts: BTreeSet<String>,
    pub(in crate::cli) include_patterns: Vec<String>,
    pub(in crate::cli) exclude_patterns: Vec<String>,
    pub(in crate::cli) ignore_patterns: Vec<String>,
}

impl WatchEventFilter {
    pub(in crate::cli) fn from_options(
        source_root: &Path,
        options: &MaterializeOptions,
    ) -> Result<Self, String> {
        let paths = GraphStatePaths::derive(source_root);
        let config_rules = read_materialization_config_rules(&paths.config_path)?;
        let mut include_patterns = config_rules.include_patterns;
        include_patterns.extend(options.include_patterns.clone());
        let mut exclude_patterns = config_rules.exclude_patterns;
        exclude_patterns.extend(options.exclude_patterns.clone());
        Ok(Self {
            source_root: source_root.to_path_buf(),
            current_dir: env::current_dir().unwrap_or_else(|_| source_root.to_path_buf()),
            excluded_parts: default_excluded_parts().into_iter().collect(),
            include_patterns,
            exclude_patterns,
            ignore_patterns: read_codebase_graph_ignore(source_root)?,
        })
    }

    pub(in crate::cli) fn relevant_paths(&self, event: &Event) -> BTreeSet<String> {
        if !watch_event_refreshes(event) {
            return BTreeSet::new();
        }
        event
            .paths
            .iter()
            .filter_map(|path| self.relevant_path(path))
            .collect()
    }

    pub(in crate::cli) fn relevant_path(&self, path: &Path) -> Option<String> {
        let relative = self.relative_event_path(path)?;
        if relative.as_os_str().is_empty() {
            return None;
        }
        if relative.components().any(|component| {
            self.excluded_parts
                .contains(component.as_os_str().to_string_lossy().as_ref())
        }) {
            return None;
        }
        let relative = relative.to_string_lossy().replace('\\', "/");
        if self.ignored_by_patterns(&relative) {
            None
        } else {
            Some(relative)
        }
    }

    pub(in crate::cli) fn relative_event_path(&self, path: &Path) -> Option<PathBuf> {
        if let Ok(relative) = path.strip_prefix(&self.source_root) {
            return Some(relative.to_path_buf());
        }
        if path.is_relative() {
            let absolute = self.current_dir.join(path);
            if let Ok(relative) = absolute.strip_prefix(&self.source_root) {
                return Some(relative.to_path_buf());
            }
            #[cfg(windows)]
            {
                let absolute = normalize_windows_verbatim_path(&absolute);
                let source_root = normalize_windows_verbatim_path(&self.source_root);
                if let Ok(relative) = absolute.strip_prefix(source_root) {
                    return Some(relative.to_path_buf());
                }
            }
            return Some(path.to_path_buf());
        }
        None
    }

    pub(in crate::cli) fn ignored_by_patterns(&self, relative_path: &str) -> bool {
        if !self.include_patterns.is_empty()
            && !watch_matches_any_pattern(relative_path, &self.include_patterns)
        {
            return true;
        }
        watch_matches_any_pattern(relative_path, &self.ignore_patterns)
            || watch_matches_any_pattern(relative_path, &self.exclude_patterns)
    }
}

#[cfg(windows)]
fn normalize_windows_verbatim_path(path: &Path) -> PathBuf {
    let normalized = path.to_string_lossy().replace('\\', "/");
    if let Some(stripped) = normalized.strip_prefix("//?/UNC/") {
        PathBuf::from(format!("//{stripped}"))
    } else if let Some(stripped) = normalized.strip_prefix("//?/") {
        PathBuf::from(stripped)
    } else {
        PathBuf::from(normalized)
    }
}

pub(in crate::cli) fn watch_event_refreshes(event: &Event) -> bool {
    matches!(
        event.kind,
        EventKind::Any
            | EventKind::Create(_)
            | EventKind::Modify(_)
            | EventKind::Remove(_)
            | EventKind::Other
            | EventKind::Access(AccessKind::Close(AccessMode::Write))
    )
}