diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
commit | c23a457e72abe608715ac76f076f47dc42af07a5 (patch) | |
tree | 2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/gix-worktree/src/stack | |
parent | Releasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-c23a457e72abe608715ac76f076f47dc42af07a5.tar.xz rustc-c23a457e72abe608715ac76f076f47dc42af07a5.zip |
Merging upstream version 1.74.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-worktree/src/stack')
-rw-r--r-- | vendor/gix-worktree/src/stack/delegate.rs | 193 | ||||
-rw-r--r-- | vendor/gix-worktree/src/stack/mod.rs | 191 | ||||
-rw-r--r-- | vendor/gix-worktree/src/stack/platform.rs | 56 | ||||
-rw-r--r-- | vendor/gix-worktree/src/stack/state/attributes.rs | 249 | ||||
-rw-r--r-- | vendor/gix-worktree/src/stack/state/ignore.rs | 224 | ||||
-rw-r--r-- | vendor/gix-worktree/src/stack/state/mod.rs | 189 |
6 files changed, 1102 insertions, 0 deletions
diff --git a/vendor/gix-worktree/src/stack/delegate.rs b/vendor/gix-worktree/src/stack/delegate.rs new file mode 100644 index 000000000..28d8ecf34 --- /dev/null +++ b/vendor/gix-worktree/src/stack/delegate.rs @@ -0,0 +1,193 @@ +use bstr::{BStr, ByteSlice}; + +use crate::{stack::State, PathIdMapping}; + +/// Various aggregate numbers related to the stack delegate itself. +#[derive(Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Statistics { + /// The amount of `std::fs::create_dir` calls. + /// + /// This only happens if we are in the respective mode to create leading directories efficiently. + pub num_mkdir_calls: usize, + /// Amount of calls to push a path element. + pub push_element: usize, + /// Amount of calls to push a directory. + pub push_directory: usize, + /// Amount of calls to pop a directory. + pub pop_directory: usize, +} + +pub(crate) type FindFn<'a> = dyn for<'b> FnMut( + &gix_hash::oid, + &'b mut Vec<u8>, + ) -> Result<gix_object::BlobRef<'b>, Box<dyn std::error::Error + Send + Sync>> + + 'a; + +pub(crate) struct StackDelegate<'a, 'find> { + pub state: &'a mut State, + pub buf: &'a mut Vec<u8>, + #[cfg_attr(not(feature = "attributes"), allow(dead_code))] + pub is_dir: bool, + pub id_mappings: &'a Vec<PathIdMapping>, + pub find: &'find mut FindFn<'find>, + pub case: gix_glob::pattern::Case, + pub statistics: &'a mut super::Statistics, +} + +impl<'a, 'find> gix_fs::stack::Delegate for StackDelegate<'a, 'find> { + fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { + self.statistics.delegate.push_directory += 1; + let dir_bstr = gix_path::into_bstr(stack.current()); + let rela_dir_cow = gix_path::to_unix_separators_on_windows( + gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( + gix_path::into_bstr(stack.root()).as_ref(), + dir_bstr.as_ref(), + None, + self.case, + ) + .expect("dir in root") + .0, + ); + let rela_dir: &BStr = if rela_dir_cow.starts_with(b"/") { + rela_dir_cow[1..].as_bstr() + } else { + rela_dir_cow.as_ref() + }; + match &mut self.state { + #[cfg(feature = "attributes")] + State::CreateDirectoryAndAttributesStack { attributes, .. } => { + attributes.push_directory( + stack.root(), + stack.current(), + rela_dir, + self.buf, + self.id_mappings, + self.find, + &mut self.statistics.attributes, + )?; + } + #[cfg(feature = "attributes")] + State::AttributesAndIgnoreStack { ignore, attributes } => { + attributes.push_directory( + stack.root(), + stack.current(), + rela_dir, + self.buf, + self.id_mappings, + &mut self.find, + &mut self.statistics.attributes, + )?; + ignore.push_directory( + stack.root(), + stack.current(), + rela_dir, + self.buf, + self.id_mappings, + &mut self.find, + self.case, + &mut self.statistics.ignore, + )? + } + #[cfg(feature = "attributes")] + State::AttributesStack(attributes) => attributes.push_directory( + stack.root(), + stack.current(), + rela_dir, + self.buf, + self.id_mappings, + &mut self.find, + &mut self.statistics.attributes, + )?, + State::IgnoreStack(ignore) => ignore.push_directory( + stack.root(), + stack.current(), + rela_dir, + self.buf, + self.id_mappings, + &mut self.find, + self.case, + &mut self.statistics.ignore, + )?, + } + Ok(()) + } + + #[cfg_attr(not(feature = "attributes"), allow(unused_variables))] + fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> { + self.statistics.delegate.push_element += 1; + match &mut self.state { + #[cfg(feature = "attributes")] + State::CreateDirectoryAndAttributesStack { + unlink_on_collision, + attributes: _, + } => create_leading_directory( + is_last_component, + stack, + self.is_dir, + &mut self.statistics.delegate.num_mkdir_calls, + *unlink_on_collision, + )?, + #[cfg(feature = "attributes")] + State::AttributesAndIgnoreStack { .. } | State::AttributesStack(_) => {} + State::IgnoreStack(_) => {} + } + Ok(()) + } + + fn pop_directory(&mut self) { + self.statistics.delegate.pop_directory += 1; + match &mut self.state { + #[cfg(feature = "attributes")] + State::CreateDirectoryAndAttributesStack { attributes, .. } => { + attributes.pop_directory(); + } + #[cfg(feature = "attributes")] + State::AttributesAndIgnoreStack { attributes, ignore } => { + attributes.pop_directory(); + ignore.pop_directory(); + } + #[cfg(feature = "attributes")] + State::AttributesStack(attributes) => { + attributes.pop_directory(); + } + State::IgnoreStack(ignore) => { + ignore.pop_directory(); + } + } + } +} + +#[cfg(feature = "attributes")] +fn create_leading_directory( + is_last_component: bool, + stack: &gix_fs::Stack, + is_dir: bool, + mkdir_calls: &mut usize, + unlink_on_collision: bool, +) -> std::io::Result<()> { + if is_last_component && !is_dir { + return Ok(()); + } + *mkdir_calls += 1; + match std::fs::create_dir(stack.current()) { + Ok(()) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { + let meta = stack.current().symlink_metadata()?; + if meta.is_dir() { + Ok(()) + } else if unlink_on_collision { + if meta.file_type().is_symlink() { + gix_fs::symlink::remove(stack.current())?; + } else { + std::fs::remove_file(stack.current())?; + } + *mkdir_calls += 1; + std::fs::create_dir(stack.current()) + } else { + Err(err) + } + } + Err(err) => Err(err), + } +} diff --git a/vendor/gix-worktree/src/stack/mod.rs b/vendor/gix-worktree/src/stack/mod.rs new file mode 100644 index 000000000..c10320199 --- /dev/null +++ b/vendor/gix-worktree/src/stack/mod.rs @@ -0,0 +1,191 @@ +#![allow(missing_docs)] +use std::path::{Path, PathBuf}; + +use bstr::{BStr, ByteSlice}; +use gix_hash::oid; + +use super::Stack; +use crate::PathIdMapping; + +/// Various aggregate numbers collected from when the corresponding [`Stack`] was instantiated. +#[derive(Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Statistics { + /// The amount of platforms created to do further matching. + pub platforms: usize, + /// Information about the stack delegate. + pub delegate: delegate::Statistics, + /// Information about attributes + #[cfg(feature = "attributes")] + pub attributes: state::attributes::Statistics, + /// Information about the ignore stack + pub ignore: state::ignore::Statistics, +} + +#[derive(Clone)] +pub enum State { + /// Useful for checkout where directories need creation, but we need to access attributes as well. + #[cfg(feature = "attributes")] + CreateDirectoryAndAttributesStack { + /// If there is a symlink or a file in our path, try to unlink it before creating the directory. + unlink_on_collision: bool, + /// State to handle attribute information + attributes: state::Attributes, + }, + /// Used when adding files, requiring access to both attributes and ignore information, for example during add operations. + #[cfg(feature = "attributes")] + AttributesAndIgnoreStack { + /// State to handle attribute information + attributes: state::Attributes, + /// State to handle exclusion information + ignore: state::Ignore, + }, + /// Used when only attributes are required, typically with fully virtual worktrees. + #[cfg(feature = "attributes")] + AttributesStack(state::Attributes), + /// Used when providing worktree status information. + IgnoreStack(state::Ignore), +} + +#[must_use] +pub struct Platform<'a> { + parent: &'a Stack, + is_dir: Option<bool>, +} + +/// Initialization +impl Stack { + /// Create a new instance with `worktree_root` being the base for all future paths we match. + /// `state` defines the capabilities of the cache. + /// The `case` configures attribute and exclusion case sensitivity at *query time*, which should match the case that + /// `state` might be configured with. + /// `buf` is used when reading files, and `id_mappings` should have been created with [`State::id_mappings_from_index()`]. + pub fn new( + worktree_root: impl Into<PathBuf>, + state: State, + case: gix_glob::pattern::Case, + buf: Vec<u8>, + id_mappings: Vec<PathIdMapping>, + ) -> Self { + let root = worktree_root.into(); + Stack { + stack: gix_fs::Stack::new(root), + state, + case, + buf, + id_mappings, + statistics: Statistics::default(), + } + } +} + +/// Entry points for attribute query +impl Stack { + /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no + /// symlinks are in that path. + /// Unless `is_dir` is known with `Some(…)`, then `relative` points to a directory itself in which case the entire resulting + /// path is created as directory. If it's not known it is assumed to be a file. + /// `find` maybe used to lookup objects from an [id mapping][crate::stack::State::id_mappings_from_index()], with mappnigs + /// + /// Provide access to cached information for that `relative` path via the returned platform. + pub fn at_path<Find, E>( + &mut self, + relative: impl AsRef<Path>, + is_dir: Option<bool>, + mut find: Find, + ) -> std::io::Result<Platform<'_>> + where + Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>, + E: std::error::Error + Send + Sync + 'static, + { + self.statistics.platforms += 1; + let mut delegate = StackDelegate { + state: &mut self.state, + buf: &mut self.buf, + is_dir: is_dir.unwrap_or(false), + id_mappings: &self.id_mappings, + find: &mut |oid, buf| Ok(find(oid, buf).map_err(Box::new)?), + case: self.case, + statistics: &mut self.statistics, + }; + self.stack + .make_relative_path_current(relative.as_ref(), &mut delegate)?; + Ok(Platform { parent: self, is_dir }) + } + + /// Obtain a platform for lookups from a repo-`relative` path, typically obtained from an index entry. `is_dir` should reflect + /// whether it's a directory or not, or left at `None` if unknown. + /// `find` maybe used to lookup objects from an [id mapping][crate::stack::State::id_mappings_from_index()]. + /// All effects are similar to [`at_path()`][Self::at_path()]. + /// + /// If `relative` ends with `/` and `is_dir` is `None`, it is automatically assumed to be a directory. + /// + /// ### Panics + /// + /// on illformed UTF8 in `relative` + pub fn at_entry<'r, Find, E>( + &mut self, + relative: impl Into<&'r BStr>, + is_dir: Option<bool>, + find: Find, + ) -> std::io::Result<Platform<'_>> + where + Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>, + E: std::error::Error + Send + Sync + 'static, + { + let relative = relative.into(); + let relative_path = gix_path::from_bstr(relative); + + self.at_path( + relative_path, + is_dir.or_else(|| relative.ends_with_str("/").then_some(true)), + find, + ) + } +} + +/// Mutation +impl Stack { + /// Reset the statistics after returning them. + pub fn take_statistics(&mut self) -> Statistics { + std::mem::take(&mut self.statistics) + } + + /// Return our state for applying changes. + pub fn state_mut(&mut self) -> &mut State { + &mut self.state + } + + /// Change the `case` of the next match to the given one. + pub fn set_case(&mut self, case: gix_glob::pattern::Case) -> &mut Self { + self.case = case; + self + } +} + +/// Access +impl Stack { + /// Return the statistics we gathered thus far. + pub fn statistics(&self) -> &Statistics { + &self.statistics + } + /// Return the state for introspection. + pub fn state(&self) -> &State { + &self.state + } + + /// Return the base path against which all entries or paths should be relative to when querying. + /// + /// Note that this path _may_ not be canonicalized. + pub fn base(&self) -> &Path { + self.stack.root() + } +} + +/// +pub mod delegate; +use delegate::StackDelegate; + +mod platform; +/// +pub mod state; diff --git a/vendor/gix-worktree/src/stack/platform.rs b/vendor/gix-worktree/src/stack/platform.rs new file mode 100644 index 000000000..3c6295f89 --- /dev/null +++ b/vendor/gix-worktree/src/stack/platform.rs @@ -0,0 +1,56 @@ +use std::path::Path; + +use bstr::ByteSlice; + +use crate::stack::Platform; + +/// Access +impl<'a> Platform<'a> { + /// The full path to `relative` will be returned for use on the file system. + pub fn path(&self) -> &'a Path { + self.parent.stack.current() + } + + /// See if the currently set entry is excluded as per exclude and git-ignore files. + /// + /// # Panics + /// + /// If the cache was configured without exclude patterns. + pub fn is_excluded(&self) -> bool { + self.matching_exclude_pattern() + .map_or(false, |m| !m.pattern.is_negative()) + } + + /// Check all exclude patterns to see if the currently set path matches any of them. + /// + /// Note that this pattern might be negated, and means this path in included. + /// + /// # Panics + /// + /// If the cache was configured without exclude patterns. + pub fn matching_exclude_pattern(&self) -> Option<gix_ignore::search::Match<'_>> { + let ignore = self.parent.state.ignore_or_panic(); + let relative_path = + gix_path::to_unix_separators_on_windows(gix_path::into_bstr(self.parent.stack.current_relative())); + ignore.matching_exclude_pattern(relative_path.as_bstr(), self.is_dir, self.parent.case) + } + + /// Match all attributes at the current path and store the result in `out`, returning `true` if at least one attribute was found. + /// + /// # Panics + /// + /// If the cache was configured without attributes. + #[cfg(feature = "attributes")] + pub fn matching_attributes(&self, out: &mut gix_attributes::search::Outcome) -> bool { + let attrs = self.parent.state.attributes_or_panic(); + let relative_path = + gix_path::to_unix_separators_on_windows(gix_path::into_bstr(self.parent.stack.current_relative())); + attrs.matching_attributes(relative_path.as_bstr(), self.parent.case, self.is_dir, out) + } +} + +impl<'a> std::fmt::Debug for Platform<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.path(), f) + } +} diff --git a/vendor/gix-worktree/src/stack/state/attributes.rs b/vendor/gix-worktree/src/stack/state/attributes.rs new file mode 100644 index 000000000..d49de1288 --- /dev/null +++ b/vendor/gix-worktree/src/stack/state/attributes.rs @@ -0,0 +1,249 @@ +use std::path::{Path, PathBuf}; + +use bstr::{BStr, ByteSlice}; +use gix_glob::pattern::Case; + +use crate::stack::delegate::FindFn; +use crate::{ + stack::state::{AttributeMatchGroup, Attributes}, + PathIdMapping, Stack, +}; + +/// Various aggregate numbers related [`Attributes`]. +#[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, +} + +/// Decide where to read `.gitattributes` files from. +/// +/// To Retrieve attribute 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. +/// +#[derive(Default, Debug, Clone, Copy)] +pub enum Source { + /// Use this when no worktree checkout is available, like in bare repositories, during clones, or when accessing blobs from + /// other parts of the history which aren't checked out. + #[default] + IdMapping, + /// Read from an id mappings and if not present, read from the worktree. + /// + /// This us typically used when *checking out* files. + IdMappingThenWorktree, + /// Read from the worktree and if not present, read them from the id mappings. + /// + /// This is typically used when *checking in* files, and it's possible for sparse worktrees not to have a `.gitattribute` file + /// checked out even though it's available in the index. + WorktreeThenIdMapping, +} + +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 + } + } +} + +/// Initialization +impl Attributes { + /// Create a new instance from an attribute match group that represents `globals`. It can more easily be created with + /// [`AttributeMatchGroup::new_globals()`]. + /// + /// * `globals` contribute first and consist of all globally available, static files. + /// * `info_attributes` is a path that should refer to `.git/info/attributes`, and it's not an error if the file doesn't exist. + /// * `case` is used to control case-sensitivity during matching. + /// * `source` specifies from where the directory-based attribute files should be loaded from. + pub fn new( + globals: AttributeMatchGroup, + info_attributes: Option<PathBuf>, + source: Source, + collection: gix_attributes::search::MetadataCollection, + ) -> Self { + Attributes { + globals, + stack: Default::default(), + info_attributes, + source, + collection, + } + } +} + +impl Attributes { + pub(crate) fn pop_directory(&mut self) { + self.stack.pop_pattern_list().expect("something to pop"); + } + + #[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], + find: &mut FindFn<'_>, + stats: &mut Statistics, + ) -> std::io::Result<()> { + let attr_path_relative = + gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); + let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); + // Git does not follow symbolic links as per documentation. + let no_follow_symlinks = false; + let read_macros_as_dir_is_root = root == dir; + + let mut added = false; + match self.source { + Source::IdMapping | Source::IdMappingThenWorktree => { + if let Ok(idx) = attr_file_in_index { + let blob = find(&id_mappings[idx].1, buf) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); + self.stack.add_patterns_buffer( + blob.data, + attr_path, + Some(Path::new("")), + &mut self.collection, + read_macros_as_dir_is_root, + ); + added = true; + stats.patterns_buffers += 1; + } + if !added && matches!(self.source, Source::IdMappingThenWorktree) { + added = self.stack.add_patterns_file( + dir.join(".gitattributes"), + no_follow_symlinks, + Some(root), + buf, + &mut self.collection, + read_macros_as_dir_is_root, + )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; + } + } + Source::WorktreeThenIdMapping => { + added = self.stack.add_patterns_file( + dir.join(".gitattributes"), + no_follow_symlinks, + Some(root), + buf, + &mut self.collection, + read_macros_as_dir_is_root, + )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; + if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) { + let blob = find(&id_mappings[idx].1, buf) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); + self.stack.add_patterns_buffer( + blob.data, + attr_path, + Some(Path::new("")), + &mut self.collection, + read_macros_as_dir_is_root, + ); + added = true; + stats.patterns_buffers += 1; + } + } + } + + // Need one stack level per component so push and pop matches, but only if this isn't the root level which is never popped. + if !added && self.info_attributes.is_none() { + self.stack + .add_patterns_buffer(&[], "<empty dummy>".into(), None, &mut self.collection, true) + } + + // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is + // by nature never popped, and follows the root, as global. + if let Some(info_attr) = self.info_attributes.take() { + let added = self.stack.add_patterns_file( + info_attr, + true, + None, + buf, + &mut self.collection, + true, /* read macros */ + )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; + } + + Ok(()) + } + + pub(crate) fn matching_attributes( + &self, + relative_path: &BStr, + case: Case, + is_dir: Option<bool>, + out: &mut gix_attributes::search::Outcome, + ) -> bool { + // assure `out` is ready to deal with possibly changed collections (append-only) + out.initialize(&self.collection); + + let groups = [&self.globals, &self.stack]; + let mut has_match = false; + groups.iter().rev().any(|group| { + has_match |= group.pattern_matching_relative_path(relative_path, case, is_dir, out); + out.is_done() + }); + has_match + } +} + +/// Attribute matching specific methods +impl Stack { + /// Creates a new container to store match outcomes for all attribute matches. + /// + /// ### Panics + /// + /// If attributes aren't configured. + pub fn attribute_matches(&self) -> gix_attributes::search::Outcome { + let mut out = gix_attributes::search::Outcome::default(); + out.initialize(&self.state.attributes_or_panic().collection); + out + } + + /// Creates a new container to store match outcomes for the given attributes. + /// + /// ### Panics + /// + /// If attributes aren't configured. + pub fn selected_attribute_matches<'a>( + &self, + given: impl IntoIterator<Item = impl Into<&'a str>>, + ) -> gix_attributes::search::Outcome { + let mut out = gix_attributes::search::Outcome::default(); + out.initialize_with_selection( + &self.state.attributes_or_panic().collection, + given.into_iter().map(Into::into), + ); + out + } + + /// Return the metadata collection that enables initializing attribute match outcomes as done in + /// [`attribute_matches()`][Stack::attribute_matches()] or [`selected_attribute_matches()`][Stack::selected_attribute_matches()] + /// + /// ### Panics + /// + /// If attributes aren't configured. + pub fn attributes_collection(&self) -> &gix_attributes::search::MetadataCollection { + &self.state.attributes_or_panic().collection + } +} diff --git a/vendor/gix-worktree/src/stack/state/ignore.rs b/vendor/gix-worktree/src/stack/state/ignore.rs new file mode 100644 index 000000000..e2a2d5a3d --- /dev/null +++ b/vendor/gix-worktree/src/stack/state/ignore.rs @@ -0,0 +1,224 @@ +use std::path::Path; + +use bstr::{BStr, ByteSlice}; +use gix_glob::pattern::Case; + +use crate::stack::delegate::FindFn; +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, + 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], + find: &mut FindFn<'_>, + 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 = find(&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 = find(&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(()) + } +} diff --git a/vendor/gix-worktree/src/stack/state/mod.rs b/vendor/gix-worktree/src/stack/state/mod.rs new file mode 100644 index 000000000..0b371425a --- /dev/null +++ b/vendor/gix-worktree/src/stack/state/mod.rs @@ -0,0 +1,189 @@ +use bstr::{BString, ByteSlice}; +use gix_glob::pattern::Case; + +use crate::{stack::State, PathIdMapping}; + +#[cfg(feature = "attributes")] +type AttributeMatchGroup = gix_attributes::Search; +type IgnoreMatchGroup = gix_ignore::Search; + +/// State related to attributes associated with files in the repository. +#[derive(Default, Clone)] +#[cfg(feature = "attributes")] +pub struct Attributes { + /// Attribute patterns which aren't tied to the repository root, hence are global, they contribute first. + globals: AttributeMatchGroup, + /// Attribute patterns that match the currently set directory (in the stack). + /// + /// Note that the root-level file is always loaded, if present, followed by, the `$GIT_DIR/info/attributes`, if present, based + /// on the location of the `info_attributes` file. + stack: AttributeMatchGroup, + /// The first time we push the root, we have to load additional information from this file if it exists along with the root attributes + /// file if possible, and keep them there throughout. + info_attributes: Option<std::path::PathBuf>, + /// A lookup table to accelerate searches. + collection: gix_attributes::search::MetadataCollection, + /// Where to read `.gitattributes` data from. + source: attributes::Source, +} + +/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read +/// ignore files from disk or from the index each time the directory changes. +#[derive(Default, Clone)] +#[allow(unused)] +pub struct Ignore { + /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to + /// be consulted. + overrides: IgnoreMatchGroup, + /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. + stack: IgnoreMatchGroup, + /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. + globals: IgnoreMatchGroup, + /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the + /// currently set directory had a pattern matched. Note that this one could be negated. + /// (index into match groups, index into list of pattern lists, index into pattern list) + matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>, + /// The name of the file to look for in directories. + pub(crate) exclude_file_name_for_directories: BString, + /// Where to read ignore files from + source: ignore::Source, +} + +/// +#[cfg(feature = "attributes")] +pub mod attributes; +/// +pub mod ignore; + +/// Initialization +impl State { + /// Configure a state to be suitable for checking out files, which only needs access to attribute files read from the index. + #[cfg(feature = "attributes")] + pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { + State::CreateDirectoryAndAttributesStack { + unlink_on_collision, + attributes, + } + } + + /// Configure a state for adding files, with support for ignore files and attribute files. + #[cfg(feature = "attributes")] + pub fn for_add(attributes: Attributes, ignore: Ignore) -> Self { + State::AttributesAndIgnoreStack { attributes, ignore } + } + + /// Configure a state for status retrieval, which needs access to ignore files only. + pub fn for_status(ignore: Ignore) -> Self { + State::IgnoreStack(ignore) + } +} + +/// Utilities +impl State { + /// Returns a vec of tuples of relative index paths along with the best usable blob OID for + /// either *ignore* or *attribute* files or both. This allows files to be accessed directly from + /// the object database without the need for a worktree checkout. + /// + /// Note that this method… + /// - ignores entries which aren't blobs. + /// - ignores ignore entries which are not skip-worktree. + /// - within merges, picks 'our' stage both for *ignore* and *attribute* files. + /// + /// * `index` is where we look for suitable files by path in order to obtain their blob hash. + /// * `paths` is the indices storage backend for paths. + /// * `case` determines if the search for files should be case-sensitive or not. + pub fn id_mappings_from_index( + &self, + index: &gix_index::State, + paths: &gix_index::PathStorageRef, + case: Case, + ) -> Vec<PathIdMapping> { + let a1_backing; + #[cfg(feature = "attributes")] + let a2_backing; + let names = match self { + State::IgnoreStack(ignore) => { + a1_backing = [( + ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), + Some(ignore.source), + )]; + a1_backing.as_ref() + } + #[cfg(feature = "attributes")] + State::AttributesAndIgnoreStack { ignore, .. } => { + a2_backing = [ + ( + ignore.exclude_file_name_for_directories.as_bytes().as_bstr(), + Some(ignore.source), + ), + (".gitattributes".into(), None), + ]; + a2_backing.as_ref() + } + #[cfg(feature = "attributes")] + State::CreateDirectoryAndAttributesStack { .. } | State::AttributesStack(_) => { + a1_backing = [(".gitattributes".into(), None)]; + a1_backing.as_ref() + } + }; + + index + .entries() + .iter() + .filter_map(move |entry| { + let path = entry.path_in(paths); + + // Stage 0 means there is no merge going on, stage 2 means it's 'our' side of the merge, but then + // there won't be a stage 0. + if entry.mode == gix_index::entry::Mode::FILE && (entry.stage() == 0 || entry.stage() == 2) { + let basename = path.rfind_byte(b'/').map_or(path, |pos| path[pos + 1..].as_bstr()); + let ignore_source = names.iter().find_map(|t| { + match case { + Case::Sensitive => basename == t.0, + Case::Fold => basename.eq_ignore_ascii_case(t.0), + } + .then_some(t.1) + })?; + if let Some(source) = ignore_source { + match source { + ignore::Source::IdMapping => {} + ignore::Source::WorktreeThenIdMappingIfNotSkipped => { + // See https://github.com/git/git/blob/master/dir.c#L912:L912 + if !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) { + return None; + } + } + }; + } + Some((path.to_owned(), entry.id)) + } else { + None + } + }) + .collect() + } + + pub(crate) fn ignore_or_panic(&self) -> &Ignore { + match self { + State::IgnoreStack(v) => v, + #[cfg(feature = "attributes")] + State::AttributesAndIgnoreStack { ignore, .. } => ignore, + #[cfg(feature = "attributes")] + State::AttributesStack(_) | State::CreateDirectoryAndAttributesStack { .. } => { + unreachable!("BUG: must not try to check excludes without it being setup") + } + } + } + + #[cfg(feature = "attributes")] + pub(crate) fn attributes_or_panic(&self) -> &Attributes { + match self { + State::AttributesStack(attributes) + | State::AttributesAndIgnoreStack { attributes, .. } + | State::CreateDirectoryAndAttributesStack { attributes, .. } => attributes, + State::IgnoreStack(_) => { + unreachable!("BUG: must not try to check excludes without it being setup") + } + } + } +} |