use std::{ ffi::OsString, path::{Path, PathBuf}, }; use bstr::{BStr, ByteSlice}; use gix_glob::search::{pattern, Pattern}; use crate::Search; /// Describes a matching pattern within a search for ignored paths. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub struct Match<'a> { /// The glob pattern itself, like `/target/*`. pub pattern: &'a gix_glob::Pattern, /// The path to the source from which the pattern was loaded, or `None` if it was specified by other means. pub source: Option<&'a Path>, /// The line at which the pattern was found in its `source` file, or the occurrence in which it was provided. pub sequence_number: usize, } /// An implementation of the [`Pattern`] trait for ignore patterns. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] pub struct Ignore; impl Pattern for Ignore { type Value = (); fn bytes_to_patterns(bytes: &[u8], _source: &std::path::Path) -> Vec> { crate::parse(bytes) .map(|(pattern, line_number)| pattern::Mapping { pattern, value: (), sequence_number: line_number, }) .collect() } } /// Instantiation of a search for ignore patterns. impl Search { /// Given `git_dir`, a `.git` repository, load static ignore patterns from `info/exclude` /// and from `excludes_file` if it is provided. /// Note that it's not considered an error if the provided `excludes_file` does not exist. pub fn from_git_dir(git_dir: &Path, excludes_file: Option, buf: &mut Vec) -> std::io::Result { let mut group = Self::default(); let follow_symlinks = true; // order matters! More important ones first. group.patterns.extend( excludes_file .and_then(|file| pattern::List::::from_file(file, None, follow_symlinks, buf).transpose()) .transpose()?, ); group.patterns.extend(pattern::List::::from_file( &git_dir.join("info").join("exclude"), None, follow_symlinks, buf, )?); Ok(group) } /// Parse a list of patterns, using slashes as path separators pub fn from_overrides(patterns: impl IntoIterator>) -> Self { Self::from_overrides_inner(&mut patterns.into_iter().map(Into::into)) } fn from_overrides_inner(patterns: &mut dyn Iterator) -> Self { Search { patterns: vec![pattern::List { patterns: patterns .enumerate() .filter_map(|(seq_id, pattern)| { let pattern = gix_path::try_into_bstr(PathBuf::from(pattern)).ok()?; gix_glob::parse(pattern.as_ref()).map(|p| pattern::Mapping { pattern: p, value: (), sequence_number: seq_id, }) }) .collect(), source: None, base: None, }], } } } /// Mutation impl Search { /// Add patterns as parsed from `bytes`, providing their `source` path and possibly their `root` path, the path they /// are relative to. This also means that `source` is contained within `root` if `root` is provided. pub fn add_patterns_buffer(&mut self, bytes: &[u8], source: impl Into, root: Option<&Path>) { self.patterns .push(pattern::List::from_bytes(bytes, source.into(), root)); } } /// Return a match if a pattern matches `relative_path`, providing a pre-computed `basename_pos` which is the /// starting position of the basename of `relative_path`. `is_dir` is true if `relative_path` is a directory. /// `case` specifies whether cases should be folded during matching or not. pub fn pattern_matching_relative_path<'a>( list: &'a gix_glob::search::pattern::List, relative_path: &BStr, basename_pos: Option, is_dir: Option, case: gix_glob::pattern::Case, ) -> Option> { let (relative_path, basename_start_pos) = list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; list.patterns.iter().rev().find_map( |pattern::Mapping { pattern, value: (), sequence_number, }| { pattern .matches_repo_relative_path( relative_path, basename_start_pos, is_dir, case, gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, ) .then_some(Match { pattern, source: list.source.as_deref(), sequence_number: *sequence_number, }) }, ) } /// Like [`pattern_matching_relative_path()`], but returns an index to the pattern /// that matched `relative_path`, instead of the match itself. pub fn pattern_idx_matching_relative_path( list: &gix_glob::search::pattern::List, relative_path: &BStr, basename_pos: Option, is_dir: Option, case: gix_glob::pattern::Case, ) -> Option { let (relative_path, basename_start_pos) = list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case)?; list.patterns.iter().enumerate().rev().find_map(|(idx, pm)| { pm.pattern .matches_repo_relative_path( relative_path, basename_start_pos, is_dir, case, gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL, ) .then_some(idx) }) } /// Matching of ignore patterns. impl Search { /// Match `relative_path` and return the first match if found. /// `is_dir` is true if `relative_path` is a directory. /// `case` specifies whether cases should be folded during matching or not. pub fn pattern_matching_relative_path( &self, relative_path: &BStr, is_dir: Option, case: gix_glob::pattern::Case, ) -> Option> { let basename_pos = relative_path.rfind(b"/").map(|p| p + 1); self.patterns .iter() .rev() .find_map(|pl| pattern_matching_relative_path(pl, relative_path, basename_pos, is_dir, case)) } }