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    stack::state::{Ignore, IgnoreMatchGroup},
9    PathIdMapping,
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 and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set.
28    #[default]
29    WorktreeThenIdMappingIfNotSkipped,
30}
31
32impl Source {
33    /// Returns non-worktree variants of `self` if `is_bare` is true.
34    pub fn adjust_for_bare(self, is_bare: bool) -> Self {
35        if is_bare {
36            Source::IdMapping
37        } else {
38            self
39        }
40    }
41}
42
43/// Various aggregate numbers related [`Ignore`].
44#[derive(Default, Clone, Copy, Debug)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct Statistics {
47    /// Amount of patterns buffers read from the index.
48    pub patterns_buffers: usize,
49    /// Amount of pattern files read from disk.
50    pub pattern_files: usize,
51    /// Amount of pattern files we tried to find on disk.
52    pub tried_pattern_files: usize,
53}
54
55impl Ignore {
56    /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory
57    /// stack be dynamic.
58    ///
59    /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory
60    /// ignore files within the repository, defaults to`.gitignore`.
61    ///
62    /// `parse` controls how to parse ignore files.
63    pub fn new(
64        overrides: IgnoreMatchGroup,
65        globals: IgnoreMatchGroup,
66        exclude_file_name_for_directories: Option<&BStr>,
67        source: Source,
68        parse: gix_ignore::search::Ignore,
69    ) -> Self {
70        Ignore {
71            overrides,
72            globals,
73            stack: Default::default(),
74            matched_directory_patterns_stack: Vec::with_capacity(6),
75            exclude_file_name_for_directories: exclude_file_name_for_directories
76                .map_or_else(|| ".gitignore".into(), ToOwned::to_owned),
77            source,
78            parse,
79        }
80    }
81}
82
83impl Ignore {
84    pub(crate) fn pop_directory(&mut self) {
85        self.matched_directory_patterns_stack.pop().expect("something to pop");
86        self.stack.patterns.pop().expect("something to pop");
87    }
88    /// The match groups from lowest priority to highest.
89    pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] {
90        [&self.globals, &self.stack, &self.overrides]
91    }
92
93    pub(crate) fn matching_exclude_pattern(
94        &self,
95        relative_path: &BStr,
96        is_dir: Option<bool>,
97        case: Case,
98    ) -> Option<gix_ignore::search::Match<'_>> {
99        let groups = self.match_groups();
100        let mut dir_match = None;
101        if let Some((source, mapping)) = self
102            .matched_directory_patterns_stack
103            .iter()
104            .rev()
105            .filter_map(|v| *v)
106            .map(|(gidx, plidx, pidx)| {
107                let list = &groups[gidx].patterns[plidx];
108                (list.source.as_deref(), &list.patterns[pidx])
109            })
110            .next()
111        {
112            let match_ = gix_ignore::search::Match {
113                pattern: &mapping.pattern,
114                sequence_number: mapping.sequence_number,
115                kind: mapping.value,
116                source,
117            };
118            if mapping.pattern.is_negative() {
119                dir_match = Some(match_);
120            } else {
121                // Note that returning here is wrong if this pattern _was_ preceded by a negative pattern that
122                // didn't match the directory, but would match now.
123                // Git does it similarly so we do too even though it's incorrect.
124                // To fix this, one would probably keep track of whether there was a preceding negative pattern, and
125                // if so we check the path in full and only use the dir match if there was no match, similar to the negative
126                // case above whose fix fortunately won't change the overall result.
127                return match_.into();
128            }
129        }
130        groups
131            .iter()
132            .rev()
133            .find_map(|group| group.pattern_matching_relative_path(relative_path, is_dir, case))
134            .or(dir_match)
135    }
136
137    /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded.
138    /// It returns a triple-index into our data structure from which a match can be reconstructed.
139    pub(crate) fn matching_exclude_pattern_no_dir(
140        &self,
141        relative_path: &BStr,
142        is_dir: Option<bool>,
143        case: Case,
144    ) -> Option<(usize, usize, usize)> {
145        let groups = self.match_groups();
146        groups.iter().enumerate().rev().find_map(|(gidx, group)| {
147            let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
148            group
149                .patterns
150                .iter()
151                .enumerate()
152                .rev()
153                .find_map(|(plidx, pl)| {
154                    gix_ignore::search::pattern_idx_matching_relative_path(
155                        pl,
156                        relative_path,
157                        basename_pos,
158                        is_dir,
159                        case,
160                    )
161                    .map(|idx| (plidx, idx))
162                })
163                .map(|(plidx, pidx)| (gidx, plidx, pidx))
164        })
165    }
166
167    #[allow(clippy::too_many_arguments)]
168    pub(crate) fn push_directory(
169        &mut self,
170        root: &Path,
171        dir: &Path,
172        rela_dir: &BStr,
173        buf: &mut Vec<u8>,
174        id_mappings: &[PathIdMapping],
175        objects: &dyn gix_object::Find,
176        case: Case,
177        stats: &mut Statistics,
178    ) -> std::io::Result<()> {
179        self.matched_directory_patterns_stack
180            .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case));
181
182        let ignore_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore");
183        let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref()));
184        match self.source {
185            Source::IdMapping => {
186                match ignore_file_in_index {
187                    Ok(idx) => {
188                        let ignore_blob = objects
189                            .find_blob(&id_mappings[idx].1, buf)
190                            .map_err(std::io::Error::other)?;
191                        let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
192                        self.stack
193                            .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")), self.parse);
194                        stats.patterns_buffers += 1;
195                    }
196                    Err(_) => {
197                        // Need one stack level per component so push and pop matches.
198                        self.stack.patterns.push(Default::default());
199                    }
200                }
201            }
202            Source::WorktreeThenIdMappingIfNotSkipped => {
203                let follow_symlinks = ignore_file_in_index.is_err();
204                let added = gix_glob::search::add_patterns_file(
205                    &mut self.stack.patterns,
206                    dir.join(".gitignore"),
207                    follow_symlinks,
208                    Some(root),
209                    buf,
210                    self.parse,
211                )?;
212                stats.pattern_files += usize::from(added);
213                stats.tried_pattern_files += 1;
214                if !added {
215                    match ignore_file_in_index {
216                        Ok(idx) => {
217                            let ignore_blob = objects
218                                .find_blob(&id_mappings[idx].1, buf)
219                                .map_err(std::io::Error::other)?;
220                            let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
221                            self.stack.add_patterns_buffer(
222                                ignore_blob.data,
223                                ignore_path,
224                                Some(Path::new("")),
225                                self.parse,
226                            );
227                            stats.patterns_buffers += 1;
228                        }
229                        Err(_) => {
230                            // Need one stack level per component so push and pop matches.
231                            self.stack.patterns.push(Default::default());
232                        }
233                    }
234                }
235            }
236        }
237        Ok(())
238    }
239}