repopilot 0.8.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::scan::config::ScanConfig;
use ignore::WalkBuilder;
use std::io;
use std::path::{Path, PathBuf};

pub(super) fn collect_paths(path: &Path, config: &ScanConfig) -> io::Result<(Vec<PathBuf>, usize)> {
    let mut file_paths = Vec::new();
    let mut dirs_count = 0usize;

    for result in build_walker(path, config) {
        let entry = result.map_err(io::Error::other)?;
        let entry_path = entry.path();

        if entry_path == path {
            continue;
        }

        let Some(file_type) = entry.file_type() else {
            continue;
        };

        if file_type.is_dir() {
            dirs_count += 1;
        } else if file_type.is_file() {
            file_paths.push(entry_path.to_path_buf());
        }
    }

    Ok((file_paths, dirs_count))
}

pub(super) fn build_walker(path: &Path, config: &ScanConfig) -> ignore::Walk {
    let root = path.to_path_buf();
    let mut ignored_paths = config.ignored_paths.clone();
    ignored_paths.extend(
        config
            .exclude_patterns
            .iter()
            .map(|pattern| pattern.trim_end_matches("/**").to_string()),
    );
    WalkBuilder::new(path)
        .hidden(false)
        .git_ignore(true)
        .git_global(true)
        .git_exclude(true)
        .add_custom_ignore_filename(".repopilotignore")
        .filter_entry(move |entry| !is_ignored_path(entry.path(), &root, &ignored_paths))
        .build()
}

fn is_ignored_path(path: &Path, root: &Path, ignored_paths: &[String]) -> bool {
    if path == root {
        return false;
    }

    ignored_paths.iter().any(|ignored_path| {
        let ignored_path = ignored_path.trim_matches('/');

        if ignored_path.is_empty() {
            return false;
        }

        path.strip_prefix(root)
            .ok()
            .and_then(|relative_path| relative_path.to_str())
            .map(|relative_path| relative_path == ignored_path)
            .unwrap_or(false)
            || path
                .file_name()
                .and_then(|name| name.to_str())
                .map(|name| name == ignored_path)
                .unwrap_or(false)
    })
}