blob: 401bcd6c95797c514bf3adb40af7a21583e11f6b [file] [log] [blame]
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<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))
})
}
#[allow(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(|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(())
}
}