use std::path::Path; use bstr::{BStr, ByteSlice}; use gix_glob::pattern::Case; use gix_object::FindExt; use crate::{ stack::state::{Ignore, IgnoreMatchGroup}, PathIdMapping, }; /// Decide where to read `.gitignore` files from. #[derive(Default, Debug, Clone, Copy)] pub enum Source { /// Retrieve ignore files from id mappings, see /// [State::id_mappings_from_index()][crate::stack::State::id_mappings_from_index()]. /// /// These mappings are typically produced from an index. /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index. /// /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts /// of the history which aren't checked out. IdMapping, /// Read from the worktree and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set. #[default] WorktreeThenIdMappingIfNotSkipped, } impl Source { /// Returns non-worktree variants of `self` if `is_bare` is true. pub fn adjust_for_bare(self, is_bare: bool) -> Self { if is_bare { Source::IdMapping } else { self } } } /// Various aggregate numbers related [`Ignore`]. #[derive(Default, Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Statistics { /// Amount of patterns buffers read from the index. pub patterns_buffers: usize, /// Amount of pattern files read from disk. pub pattern_files: usize, /// Amount of pattern files we tried to find on disk. pub tried_pattern_files: usize, } impl Ignore { /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory /// stack be dynamic. /// /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory /// ignore files within the repository, defaults to`.gitignore`. pub fn new( overrides: IgnoreMatchGroup, globals: IgnoreMatchGroup, exclude_file_name_for_directories: Option<&BStr>, source: Source, ) -> Self { Ignore { overrides, globals, stack: Default::default(), matched_directory_patterns_stack: Vec::with_capacity(6), exclude_file_name_for_directories: exclude_file_name_for_directories .map_or_else(|| ".gitignore".into(), ToOwned::to_owned), source, } } } impl Ignore { pub(crate) fn pop_directory(&mut self) { self.matched_directory_patterns_stack.pop().expect("something to pop"); self.stack.patterns.pop().expect("something to pop"); } /// The match groups from lowest priority to highest. pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] { [&self.globals, &self.stack, &self.overrides] } pub(crate) fn matching_exclude_pattern( &self, relative_path: &BStr, is_dir: Option, case: Case, ) -> Option> { let groups = self.match_groups(); let mut dir_match = None; if let Some((source, mapping)) = self .matched_directory_patterns_stack .iter() .rev() .filter_map(|v| *v) .map(|(gidx, plidx, pidx)| { let list = &groups[gidx].patterns[plidx]; (list.source.as_deref(), &list.patterns[pidx]) }) .next() { let match_ = gix_ignore::search::Match { pattern: &mapping.pattern, sequence_number: mapping.sequence_number, source, }; if mapping.pattern.is_negative() { dir_match = Some(match_); } else { // Note that returning here is wrong if this pattern _was_ preceded by a negative pattern that // didn't match the directory, but would match now. // Git does it similarly so we do too even though it's incorrect. // To fix this, one would probably keep track of whether there was a preceding negative pattern, and // if so we check the path in full and only use the dir match if there was no match, similar to the negative // case above whose fix fortunately won't change the overall result. return match_.into(); } } groups .iter() .rev() .find_map(|group| group.pattern_matching_relative_path(relative_path, is_dir, case)) .or(dir_match) } /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded. /// It returns a triple-index into our data structure from which a match can be reconstructed. pub(crate) fn matching_exclude_pattern_no_dir( &self, relative_path: &BStr, is_dir: Option, case: Case, ) -> Option<(usize, usize, usize)> { let groups = self.match_groups(); groups.iter().enumerate().rev().find_map(|(gidx, group)| { let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); group .patterns .iter() .enumerate() .rev() .find_map(|(plidx, pl)| { gix_ignore::search::pattern_idx_matching_relative_path( pl, relative_path, basename_pos, is_dir, case, ) .map(|idx| (plidx, idx)) }) .map(|(plidx, pidx)| (gidx, plidx, pidx)) }) } #[allow(clippy::too_many_arguments)] pub(crate) fn push_directory( &mut self, root: &Path, dir: &Path, rela_dir: &BStr, buf: &mut Vec, id_mappings: &[PathIdMapping], objects: &dyn gix_object::Find, case: Case, stats: &mut Statistics, ) -> std::io::Result<()> { self.matched_directory_patterns_stack .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case)); let ignore_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore"); let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref())); match self.source { Source::IdMapping => { match ignore_file_in_index { Ok(idx) => { let ignore_blob = objects .find_blob(&id_mappings[idx].1, buf) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); self.stack .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); stats.patterns_buffers += 1; } Err(_) => { // Need one stack level per component so push and pop matches. self.stack.patterns.push(Default::default()) } } } Source::WorktreeThenIdMappingIfNotSkipped => { let follow_symlinks = ignore_file_in_index.is_err(); let added = gix_glob::search::add_patterns_file( &mut self.stack.patterns, dir.join(".gitignore"), follow_symlinks, Some(root), buf, )?; stats.pattern_files += usize::from(added); stats.tried_pattern_files += 1; if !added { match ignore_file_in_index { Ok(idx) => { let ignore_blob = objects .find_blob(&id_mappings[idx].1, buf) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); self.stack .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); stats.patterns_buffers += 1; } Err(_) => { // Need one stack level per component so push and pop matches. self.stack.patterns.push(Default::default()) } } } } } Ok(()) } }