rs-dot-ignore 0.1.0

A Rust library and CLI utility for **efficient directory walking with `.ignore`-style file exclusion** — inspired by `.gitignore` behavior.
Documentation
pub mod constances;
pub mod ignoreset;

use crate::{constances::DOT_IGNORE_FILE_NAME, ignoreset::IgnoreSet};
use std::{
    fs::{self, DirEntry},
    path::PathBuf,
};

/// Walk Dir
///
///
///
/// ```
/// let base_dir = std::env::current_dir().unwrap();
///
/// rs_dot_ignore::walk_dir(base_dir.to_str().unwrap(), |entry| {
///     let entry_path = entry.path();
///     let path = entry_path.to_str().unwrap();
///
///     if entry_path.is_dir() {
///         println!("Dir : {path}");
///     } else {
///         println!("File: {path}");
///     }
/// });
/// ```
///
pub fn walk_dir(dir_path: &str, callback: impl Fn(DirEntry) -> ()) {
    let dir_path = PathBuf::from(dir_path);

    let mut ignore_set: Vec<IgnoreSet> = Vec::new();

    dot_ignore_recursive_walk_dir(&dir_path, &dir_path, &mut ignore_set, &callback);
}

/// Dot Ignore Recursive Walk Dir
///
///
fn dot_ignore_recursive_walk_dir(
    base_dir: &PathBuf,
    current_dir: &PathBuf,
    ignore_set: &mut Vec<IgnoreSet>,
    callback: &impl Fn(DirEntry) -> (),
) {
    // setup scope ignore set
    let mut new_ignore_set = IgnoreSet::new(current_dir);

    read_dot_ignore(&current_dir, &mut new_ignore_set);

    new_ignore_set.compile();

    ignore_set.push(new_ignore_set);

    // read dir
    for entry in std::fs::read_dir(&current_dir)
        .expect("Failed to read_dir")
        .flatten()
    {
        let entry_path = entry.path();
        
        if is_matched(ignore_set, current_dir, &entry_path) {
            continue;
        }

        callback(entry);

        // recursive call
        if entry_path.is_dir() {
            dot_ignore_recursive_walk_dir(base_dir, &entry_path, ignore_set, callback);
        }
    }

    // cleaning scope ignore set
    ignore_set.pop();
}

/// Read Dot Ignore
///
///
fn read_dot_ignore(dir_path: &PathBuf, ignore_set: &mut IgnoreSet) {
    let contents = fs::read_to_string(dir_path.join(DOT_IGNORE_FILE_NAME)).unwrap_or_default();



    for line in contents.lines() {
        let line = line.trim();

        if line.is_empty() || line.starts_with("#") {
            continue;
        }

        let line = line
            .trim_start_matches("/")
            .trim_start_matches("./");

        ignore_set.insert(line);
    }
}


/// Is Matched
/// 
/// 
/// 
fn is_matched(ignore_set: &Vec<IgnoreSet>, _current_dir: &PathBuf, entry_path: &PathBuf) -> bool {
    for ignore in ignore_set.iter() {
        let relative_path = entry_path.strip_prefix(&ignore.dir).unwrap();
        let relative_path = relative_path.to_str().unwrap();

        if ignore.is_matched(relative_path) {
            // println!("          > {relative_path}"); // Debug
            return true;
        }
    }
    false
}