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