235 lines · 9.1 KB · Rust
RawDownload
use std::path::Path; use bstr::{BStr, ByteSlice};use gix_glob::pattern::Case;use gix_object::FindExt; use crate::{    PathIdMapping,    stack::state::{Ignore, IgnoreMatchGroup},}; /// Specify how to parse ignore patterns.pub use gix_ignore::search::Ignore as ParseIgnore; /// 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`.    ///    /// `parse` controls how to parse ignore files.    pub fn new(        overrides: IgnoreMatchGroup,        globals: IgnoreMatchGroup,        exclude_file_name_for_directories: Option<&BStr>,        source: Source,        parse: gix_ignore::search::Ignore,    ) -> 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,            parse,        }    }} 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<bool>,        case: Case,    ) -> Option<gix_ignore::search::Match<'_>> {        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,                kind: mapping.value,                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<bool>,        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))        })    }     #[expect(clippy::too_many_arguments)]    pub(crate) fn push_directory(        &mut self,        root: &Path,        dir: &Path,        rela_dir: &BStr,        buf: &mut Vec<u8>,        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(std::io::Error::other)?;                        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("")), self.parse);                        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,                    self.parse,                )?;                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(std::io::Error::other)?;                            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("")),                                self.parse,                            );                            stats.patterns_buffers += 1;                        }                        Err(_) => {                            // Need one stack level per component so push and pop matches.                            self.stack.patterns.push(Default::default());                        }                    }                }            }        }        Ok(())    }}