blob: a527b447221159556076528bebb652fff49f4eb0 [file] [log] [blame]
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 kind of pattern this match represents.
pub kind: crate::Kind,
/// 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 = crate::Kind;
fn bytes_to_patterns(bytes: &[u8], _source: &std::path::Path) -> Vec<pattern::Mapping<Self::Value>> {
crate::parse(bytes)
.map(|(pattern, line_number, kind)| pattern::Mapping {
pattern,
value: kind,
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<PathBuf>, buf: &mut Vec<u8>) -> std::io::Result<Self> {
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::<Ignore>::from_file(file, None, follow_symlinks, buf).transpose())
.transpose()?,
);
group.patterns.extend(pattern::List::<Ignore>::from_file(
&git_dir.join("info").join("exclude"),
None,
follow_symlinks,
buf,
)?);
Ok(group)
}
/// Parse a list of ignore patterns, using slashes as path separators.
pub fn from_overrides(patterns: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
Self::from_overrides_inner(&mut patterns.into_iter().map(Into::into))
}
fn from_overrides_inner(patterns: &mut dyn Iterator<Item = OsString>) -> 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()?;
crate::parse(pattern.as_ref())
.next()
.map(|(p, _seq_id, kind)| pattern::Mapping {
pattern: p,
value: kind,
sequence_number: seq_id + 1,
})
})
.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<PathBuf>, 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<Ignore>,
relative_path: &BStr,
basename_pos: Option<usize>,
is_dir: Option<bool>,
case: gix_glob::pattern::Case,
) -> Option<Match<'a>> {
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: kind,
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,
kind: *kind,
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<Ignore>,
relative_path: &BStr,
basename_pos: Option<usize>,
is_dir: Option<bool>,
case: gix_glob::pattern::Case,
) -> Option<usize> {
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<bool>,
case: gix_glob::pattern::Case,
) -> Option<Match<'_>> {
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))
}
}