summaryrefslogtreecommitdiffstats
path: root/vendor/gix-worktree/src/stack
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 18:31:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 18:31:44 +0000
commitc23a457e72abe608715ac76f076f47dc42af07a5 (patch)
tree2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/gix-worktree/src/stack
parentReleasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff)
downloadrustc-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.rs193
-rw-r--r--vendor/gix-worktree/src/stack/mod.rs191
-rw-r--r--vendor/gix-worktree/src/stack/platform.rs56
-rw-r--r--vendor/gix-worktree/src/stack/state/attributes.rs249
-rw-r--r--vendor/gix-worktree/src/stack/state/ignore.rs224
-rw-r--r--vendor/gix-worktree/src/stack/state/mod.rs189
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")
+ }
+ }
+ }
+}