1use ignore::gitignore::Gitignore;
  2use std::{ffi::OsStr, path::Path, sync::Arc};
  3
  4#[derive(Clone, Debug)]
  5pub struct IgnoreStack {
  6    pub repo_root: Option<Arc<Path>>,
  7    pub top: Arc<IgnoreStackEntry>,
  8}
  9
 10#[derive(Debug)]
 11pub enum IgnoreStackEntry {
 12    None,
 13    Global {
 14        ignore: Arc<Gitignore>,
 15    },
 16    Some {
 17        abs_base_path: Arc<Path>,
 18        ignore: Arc<Gitignore>,
 19        parent: Arc<IgnoreStackEntry>,
 20    },
 21    All,
 22}
 23
 24impl IgnoreStack {
 25    pub fn none() -> Self {
 26        Self {
 27            repo_root: None,
 28            top: Arc::new(IgnoreStackEntry::None),
 29        }
 30    }
 31
 32    pub fn all() -> Self {
 33        Self {
 34            repo_root: None,
 35            top: Arc::new(IgnoreStackEntry::All),
 36        }
 37    }
 38
 39    pub fn global(ignore: Arc<Gitignore>) -> Self {
 40        Self {
 41            repo_root: None,
 42            top: Arc::new(IgnoreStackEntry::Global { ignore }),
 43        }
 44    }
 45
 46    pub fn append(self, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Self {
 47        let top = match self.top.as_ref() {
 48            IgnoreStackEntry::All => self.top.clone(),
 49            _ => Arc::new(IgnoreStackEntry::Some {
 50                abs_base_path,
 51                ignore,
 52                parent: self.top.clone(),
 53            }),
 54        };
 55        Self {
 56            repo_root: self.repo_root,
 57            top,
 58        }
 59    }
 60
 61    pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool {
 62        if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
 63            return true;
 64        }
 65
 66        match self.top.as_ref() {
 67            IgnoreStackEntry::None => false,
 68            IgnoreStackEntry::All => true,
 69            IgnoreStackEntry::Global { ignore } => {
 70                let combined_path;
 71                let abs_path = if let Some(repo_root) = self.repo_root.as_ref() {
 72                    combined_path = ignore.path().join(
 73                        abs_path
 74                            .strip_prefix(repo_root)
 75                            .expect("repo root should be a parent of matched path"),
 76                    );
 77                    &combined_path
 78                } else {
 79                    abs_path
 80                };
 81                match ignore.matched(abs_path, is_dir) {
 82                    ignore::Match::None => false,
 83                    ignore::Match::Ignore(_) => true,
 84                    ignore::Match::Whitelist(_) => false,
 85                }
 86            }
 87            IgnoreStackEntry::Some {
 88                abs_base_path,
 89                ignore,
 90                parent: prev,
 91            } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
 92                ignore::Match::None => IgnoreStack {
 93                    repo_root: self.repo_root.clone(),
 94                    top: prev.clone(),
 95                }
 96                .is_abs_path_ignored(abs_path, is_dir),
 97                ignore::Match::Ignore(_) => true,
 98                ignore::Match::Whitelist(_) => false,
 99            },
100        }
101    }
102}