Skip to main content

gix_worktree/stack/state/
attributes.rs

1use std::path::{Path, PathBuf};
2
3use bstr::{BStr, ByteSlice};
4use gix_glob::pattern::Case;
5use gix_object::FindExt;
6
7use crate::{
8    PathIdMapping, Stack,
9    stack::state::{AttributeMatchGroup, Attributes},
10};
11
12/// Various aggregate numbers related [`Attributes`].
13#[derive(Default, Clone, Copy, Debug)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct Statistics {
16    /// Amount of patterns buffers read from the index.
17    pub patterns_buffers: usize,
18    /// Amount of pattern files read from disk.
19    pub pattern_files: usize,
20    /// Amount of pattern files we tried to find on disk.
21    pub tried_pattern_files: usize,
22}
23
24/// Decide where to read `.gitattributes` files from.
25///
26/// To Retrieve attribute files from id mappings, see
27/// [State::id_mappings_from_index()][crate::stack::State::id_mappings_from_index()].
28///
29/// These mappings are typically produced from an index.
30/// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index.
31///
32#[derive(Default, Debug, Clone, Copy)]
33pub enum Source {
34    /// Use this when no worktree checkout is available, like in bare repositories, during clones, or when accessing blobs from
35    /// other parts of the history which aren't checked out.
36    #[default]
37    IdMapping,
38    /// Read from an id mappings and if not present, read from the worktree.
39    ///
40    /// This us typically used when *checking out* files.
41    IdMappingThenWorktree,
42    /// Read from the worktree and if not present, read them from the id mappings.
43    ///
44    /// This is typically used when *checking in* files, and it's possible for sparse worktrees not to have a `.gitattribute` file
45    /// checked out even though it's available in the index.
46    WorktreeThenIdMapping,
47}
48
49impl Source {
50    /// Returns non-worktree variants of `self` if `is_bare` is true.
51    pub fn adjust_for_bare(self, is_bare: bool) -> Self {
52        if is_bare { Source::IdMapping } else { self }
53    }
54}
55
56/// Initialization
57impl Attributes {
58    /// Create a new instance from an attribute match group that represents `globals`. It can more easily be created with
59    /// [`AttributeMatchGroup::new_globals()`].
60    ///
61    /// * `globals` contribute first and consist of all globally available, static files.
62    /// * `info_attributes` is a path that should refer to `.git/info/attributes`, and it's not an error if the file doesn't exist.
63    /// * `case` is used to control case-sensitivity during matching.
64    /// * `source` specifies from where the directory-based attribute files should be loaded from.
65    pub fn new(
66        globals: AttributeMatchGroup,
67        info_attributes: Option<PathBuf>,
68        source: Source,
69        collection: gix_attributes::search::MetadataCollection,
70    ) -> Self {
71        Attributes {
72            globals,
73            stack: Default::default(),
74            info_attributes,
75            source,
76            collection,
77        }
78    }
79}
80
81impl Attributes {
82    pub(crate) fn pop_directory(&mut self) {
83        self.stack.pop_pattern_list().expect("something to pop");
84    }
85
86    #[expect(clippy::too_many_arguments)]
87    pub(crate) fn push_directory(
88        &mut self,
89        root: &Path,
90        dir: &Path,
91        rela_dir: &BStr,
92        buf: &mut Vec<u8>,
93        id_mappings: &[PathIdMapping],
94        objects: &dyn gix_object::Find,
95        stats: &mut Statistics,
96    ) -> std::io::Result<()> {
97        let attr_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes");
98        let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref()));
99        // Git does not follow symbolic links as per documentation.
100        let no_follow_symlinks = false;
101        let read_macros_as_dir_is_root = root == dir;
102
103        let mut added = false;
104        match self.source {
105            Source::IdMapping | Source::IdMappingThenWorktree => {
106                if let Ok(idx) = attr_file_in_index {
107                    let blob = objects
108                        .find_blob(&id_mappings[idx].1, buf)
109                        .map_err(std::io::Error::other)?;
110                    let attr_path =
111                        gix_path::from_bstring(attr_path_relative.into_owned()).map_err(std::io::Error::other)?;
112                    self.stack.add_patterns_buffer(
113                        blob.data,
114                        attr_path,
115                        Some(Path::new("")),
116                        &mut self.collection,
117                        read_macros_as_dir_is_root,
118                    )?;
119                    added = true;
120                    stats.patterns_buffers += 1;
121                }
122                if !added && matches!(self.source, Source::IdMappingThenWorktree) {
123                    added = self.stack.add_patterns_file(
124                        dir.join(".gitattributes"),
125                        no_follow_symlinks,
126                        Some(root),
127                        buf,
128                        &mut self.collection,
129                        read_macros_as_dir_is_root,
130                    )?;
131                    stats.pattern_files += usize::from(added);
132                    stats.tried_pattern_files += 1;
133                }
134            }
135            Source::WorktreeThenIdMapping => {
136                added = self.stack.add_patterns_file(
137                    dir.join(".gitattributes"),
138                    no_follow_symlinks,
139                    Some(root),
140                    buf,
141                    &mut self.collection,
142                    read_macros_as_dir_is_root,
143                )?;
144                stats.pattern_files += usize::from(added);
145                stats.tried_pattern_files += 1;
146                if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) {
147                    let blob = objects
148                        .find_blob(&id_mappings[idx].1, buf)
149                        .map_err(std::io::Error::other)?;
150                    let attr_path =
151                        gix_path::from_bstring(attr_path_relative.into_owned()).map_err(std::io::Error::other)?;
152                    self.stack.add_patterns_buffer(
153                        blob.data,
154                        attr_path,
155                        Some(Path::new("")),
156                        &mut self.collection,
157                        read_macros_as_dir_is_root,
158                    )?;
159                    added = true;
160                    stats.patterns_buffers += 1;
161                }
162            }
163        }
164
165        // Need one stack level per component so push and pop matches, but only if this isn't the root level which is never popped.
166        if !added && self.info_attributes.is_none() {
167            self.stack
168                .add_patterns_buffer(&[], "<empty dummy>".into(), None, &mut self.collection, true)?;
169        }
170
171        // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is
172        // by nature never popped, and follows the root, as global.
173        if let Some(info_attr) = self.info_attributes.take() {
174            let added = self.stack.add_patterns_file(
175                info_attr,
176                true,
177                None,
178                buf,
179                &mut self.collection,
180                true, /* read macros */
181            )?;
182            stats.pattern_files += usize::from(added);
183            stats.tried_pattern_files += 1;
184        }
185
186        Ok(())
187    }
188
189    pub(crate) fn matching_attributes(
190        &self,
191        relative_path: &BStr,
192        case: Case,
193        is_dir: Option<bool>,
194        out: &mut gix_attributes::search::Outcome,
195    ) -> bool {
196        // assure `out` is ready to deal with possibly changed collections (append-only)
197        out.initialize(&self.collection);
198
199        let groups = [&self.globals, &self.stack];
200        let mut has_match = false;
201        groups.iter().rev().any(|group| {
202            has_match |= group.pattern_matching_relative_path(relative_path, case, is_dir, out);
203            out.is_done()
204        });
205        has_match
206    }
207}
208
209/// Attribute matching specific methods
210impl Stack {
211    /// Creates a new container to store match outcomes for all attribute matches.
212    ///
213    /// ### Panics
214    ///
215    /// If attributes aren't configured.
216    pub fn attribute_matches(&self) -> gix_attributes::search::Outcome {
217        let mut out = gix_attributes::search::Outcome::default();
218        out.initialize(&self.state.attributes_or_panic().collection);
219        out
220    }
221
222    /// Creates a new container to store match outcomes for the given attributes.
223    ///
224    /// ### Panics
225    ///
226    /// If attributes aren't configured.
227    pub fn selected_attribute_matches<'a>(
228        &self,
229        given: impl IntoIterator<Item = impl Into<&'a str>>,
230    ) -> gix_attributes::search::Outcome {
231        let mut out = gix_attributes::search::Outcome::default();
232        out.initialize_with_selection(
233            &self.state.attributes_or_panic().collection,
234            given.into_iter().map(Into::into),
235        );
236        out
237    }
238
239    /// Return the metadata collection that enables initializing attribute match outcomes as done in
240    /// [`attribute_matches()`][Stack::attribute_matches()] or [`selected_attribute_matches()`][Stack::selected_attribute_matches()]
241    ///
242    /// ### Panics
243    ///
244    /// If attributes aren't configured.
245    pub fn attributes_collection(&self) -> &gix_attributes::search::MetadataCollection {
246        &self.state.attributes_or_panic().collection
247    }
248}