Skip to main content

gix_worktree/stack/state/
ignore.rs

1use std::path::Path;
2
3use bstr::{BStr, ByteSlice};
4use gix_glob::pattern::Case;
5use gix_object::FindExt;
6
7use crate::{
8    PathIdMapping,
9    stack::state::{Ignore, IgnoreMatchGroup},
10};
11
12/// Specify how to parse ignore patterns.
13pub use gix_ignore::search::Ignore as ParseIgnore;
14
15/// Decide where to read `.gitignore` files from.
16#[derive(Default, Debug, Clone, Copy)]
17pub enum Source {
18    /// Retrieve ignore files from id mappings, see
19    /// [State::id_mappings_from_index()][crate::stack::State::id_mappings_from_index()].
20    ///
21    /// These mappings are typically produced from an index.
22    /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index.
23    ///
24    /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts
25    /// of the history which aren't checked out.
26    IdMapping,
27    /// Read from the worktree without following symlinks, falling back to id mappings
28    /// for entries with the skip-worktree bit set.
29    #[default]
30    WorktreeThenIdMappingIfNotSkipped,
31}
32
33impl Source {
34    /// Returns non-worktree variants of `self` if `is_bare` is true.
35    pub fn adjust_for_bare(self, is_bare: bool) -> Self {
36        if is_bare { Source::IdMapping } else { self }
37    }
38}
39
40/// Various aggregate numbers related [`Ignore`].
41#[derive(Default, Clone, Copy, Debug)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct Statistics {
44    /// Amount of patterns buffers read from the index.
45    pub patterns_buffers: usize,
46    /// Amount of pattern files read from disk.
47    pub pattern_files: usize,
48    /// Amount of pattern files we tried to find on disk.
49    pub tried_pattern_files: usize,
50}
51
52impl Ignore {
53    /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory
54    /// stack be dynamic.
55    ///
56    /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory
57    /// ignore files within the repository, defaults to`.gitignore`.
58    ///
59    /// `parse` controls how to parse ignore files.
60    pub fn new(
61        overrides: IgnoreMatchGroup,
62        globals: IgnoreMatchGroup,
63        exclude_file_name_for_directories: Option<&BStr>,
64        source: Source,
65        parse: gix_ignore::search::Ignore,
66    ) -> Self {
67        Ignore {
68            overrides,
69            globals,
70            stack: Default::default(),
71            matched_directory_patterns_stack: Vec::with_capacity(6),
72            exclude_file_name_for_directories: exclude_file_name_for_directories
73                .map_or_else(|| ".gitignore".into(), ToOwned::to_owned),
74            source,
75            parse,
76        }
77    }
78}
79
80impl Ignore {
81    pub(crate) fn pop_directory(&mut self) {
82        self.matched_directory_patterns_stack.pop().expect("something to pop");
83        self.stack.patterns.pop().expect("something to pop");
84    }
85    /// The match groups from lowest priority to highest.
86    pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] {
87        [&self.globals, &self.stack, &self.overrides]
88    }
89
90    pub(crate) fn matching_exclude_pattern(
91        &self,
92        relative_path: &BStr,
93        is_dir: Option<bool>,
94        case: Case,
95    ) -> Option<gix_ignore::search::Match<'_>> {
96        let groups = self.match_groups();
97        let mut dir_match = None;
98        for (source, mapping) in self
99            .matched_directory_patterns_stack
100            .iter()
101            .rev()
102            .filter_map(|v| *v)
103            .map(|(gidx, plidx, pidx)| {
104                let list = &groups[gidx].patterns[plidx];
105                (list.source.as_deref(), &list.patterns[pidx])
106            })
107        {
108            let match_ = gix_ignore::search::Match {
109                pattern: &mapping.pattern,
110                sequence_number: mapping.sequence_number,
111                kind: mapping.value,
112                source,
113            };
114            if mapping.pattern.is_negative() {
115                dir_match.get_or_insert(match_);
116            } else {
117                // An excluded ancestor excludes all descendants, even if a nearer directory matches a negation.
118                // For example, `tld/` in the root `.gitignore` still excludes `tld/sd/file` when
119                // `tld/.gitignore` contains `!sd/`. Our stack records that negation for `tld/sd`,
120                // but Git doesn't consult ignore files inside the excluded `tld` directory.
121                return match_.into();
122            }
123        }
124        groups
125            .iter()
126            .rev()
127            .find_map(|group| group.pattern_matching_relative_path(relative_path, is_dir, case))
128            .or(dir_match)
129    }
130
131    /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded.
132    /// It returns a triple-index into our data structure from which a match can be reconstructed.
133    pub(crate) fn matching_exclude_pattern_no_dir(
134        &self,
135        relative_path: &BStr,
136        is_dir: Option<bool>,
137        case: Case,
138    ) -> Option<(usize, usize, usize)> {
139        let groups = self.match_groups();
140        groups.iter().enumerate().rev().find_map(|(gidx, group)| {
141            let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
142            group
143                .patterns
144                .iter()
145                .enumerate()
146                .rev()
147                .find_map(|(plidx, pl)| {
148                    gix_ignore::search::pattern_idx_matching_relative_path(
149                        pl,
150                        relative_path,
151                        basename_pos,
152                        is_dir,
153                        case,
154                    )
155                    .map(|idx| (plidx, idx))
156                })
157                .map(|(plidx, pidx)| (gidx, plidx, pidx))
158        })
159    }
160
161    #[expect(clippy::too_many_arguments)]
162    pub(crate) fn push_directory(
163        &mut self,
164        root: &Path,
165        dir: &Path,
166        rela_dir: &BStr,
167        buf: &mut Vec<u8>,
168        id_mappings: &[PathIdMapping],
169        objects: &dyn gix_object::Find,
170        case: Case,
171        stats: &mut Statistics,
172    ) -> std::io::Result<()> {
173        self.matched_directory_patterns_stack
174            .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case));
175
176        let ignore_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore");
177        let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref()));
178        match self.source {
179            Source::IdMapping => {
180                match ignore_file_in_index {
181                    Ok(idx) => {
182                        let ignore_blob = objects
183                            .find_blob(&id_mappings[idx].1, buf)
184                            .map_err(std::io::Error::other)?;
185                        let ignore_path =
186                            gix_path::from_bstring(ignore_path_relative.into_owned()).map_err(std::io::Error::other)?;
187                        self.stack.add_patterns_buffer(
188                            ignore_blob.data,
189                            ignore_path,
190                            Some(Path::new("")),
191                            self.parse,
192                        )?;
193                        stats.patterns_buffers += 1;
194                    }
195                    Err(_) => {
196                        // Need one stack level per component so push and pop matches.
197                        self.stack.patterns.push(Default::default());
198                    }
199                }
200            }
201            Source::WorktreeThenIdMappingIfNotSkipped => {
202                let added = gix_glob::search::add_patterns_file(
203                    &mut self.stack.patterns,
204                    dir.join(".gitignore"),
205                    false,
206                    Some(root),
207                    buf,
208                    self.parse,
209                )?;
210                stats.pattern_files += usize::from(added);
211                stats.tried_pattern_files += 1;
212                if !added {
213                    match ignore_file_in_index {
214                        Ok(idx) => {
215                            let ignore_blob = objects
216                                .find_blob(&id_mappings[idx].1, buf)
217                                .map_err(std::io::Error::other)?;
218                            let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned())
219                                .map_err(std::io::Error::other)?;
220                            self.stack.add_patterns_buffer(
221                                ignore_blob.data,
222                                ignore_path,
223                                Some(Path::new("")),
224                                self.parse,
225                            )?;
226                            stats.patterns_buffers += 1;
227                        }
228                        Err(_) => {
229                            // Need one stack level per component so push and pop matches.
230                            self.stack.patterns.push(Default::default());
231                        }
232                    }
233                }
234            }
235        }
236        Ok(())
237    }
238}