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/// Decide where to read `.gitignore` files from.
13#[derive(Default, Debug, Clone, Copy)]
14pub enum Source {
15    /// Retrieve ignore files from id mappings, see
16    /// [State::id_mappings_from_index()][crate::stack::State::id_mappings_from_index()].
17    ///
18    /// These mappings are typically produced from an index.
19    /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index.
20    ///
21    /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts
22    /// of the history which aren't checked out.
23    IdMapping,
24    /// Read from the worktree and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set.
25    #[default]
26    WorktreeThenIdMappingIfNotSkipped,
27}
28
29impl Source {
30    /// Returns non-worktree variants of `self` if `is_bare` is true.
31    pub fn adjust_for_bare(self, is_bare: bool) -> Self {
32        if is_bare {
33            Source::IdMapping
34        } else {
35            self
36        }
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    pub fn new(
59        overrides: IgnoreMatchGroup,
60        globals: IgnoreMatchGroup,
61        exclude_file_name_for_directories: Option<&BStr>,
62        source: Source,
63    ) -> Self {
64        Ignore {
65            overrides,
66            globals,
67            stack: Default::default(),
68            matched_directory_patterns_stack: Vec::with_capacity(6),
69            exclude_file_name_for_directories: exclude_file_name_for_directories
70                .map_or_else(|| ".gitignore".into(), ToOwned::to_owned),
71            source,
72        }
73    }
74}
75
76impl Ignore {
77    pub(crate) fn pop_directory(&mut self) {
78        self.matched_directory_patterns_stack.pop().expect("something to pop");
79        self.stack.patterns.pop().expect("something to pop");
80    }
81    /// The match groups from lowest priority to highest.
82    pub(crate) fn match_groups(&self) -> [&IgnoreMatchGroup; 3] {
83        [&self.globals, &self.stack, &self.overrides]
84    }
85
86    pub(crate) fn matching_exclude_pattern(
87        &self,
88        relative_path: &BStr,
89        is_dir: Option<bool>,
90        case: Case,
91    ) -> Option<gix_ignore::search::Match<'_>> {
92        let groups = self.match_groups();
93        let mut dir_match = None;
94        if let Some((source, mapping)) = self
95            .matched_directory_patterns_stack
96            .iter()
97            .rev()
98            .filter_map(|v| *v)
99            .map(|(gidx, plidx, pidx)| {
100                let list = &groups[gidx].patterns[plidx];
101                (list.source.as_deref(), &list.patterns[pidx])
102            })
103            .next()
104        {
105            let match_ = gix_ignore::search::Match {
106                pattern: &mapping.pattern,
107                sequence_number: mapping.sequence_number,
108                kind: mapping.value,
109                source,
110            };
111            if mapping.pattern.is_negative() {
112                dir_match = Some(match_);
113            } else {
114                // Note that returning here is wrong if this pattern _was_ preceded by a negative pattern that
115                // didn't match the directory, but would match now.
116                // Git does it similarly so we do too even though it's incorrect.
117                // To fix this, one would probably keep track of whether there was a preceding negative pattern, and
118                // if so we check the path in full and only use the dir match if there was no match, similar to the negative
119                // case above whose fix fortunately won't change the overall result.
120                return match_.into();
121            }
122        }
123        groups
124            .iter()
125            .rev()
126            .find_map(|group| group.pattern_matching_relative_path(relative_path, is_dir, case))
127            .or(dir_match)
128    }
129
130    /// Like `matching_exclude_pattern()` but without checking if the current directory is excluded.
131    /// It returns a triple-index into our data structure from which a match can be reconstructed.
132    pub(crate) fn matching_exclude_pattern_no_dir(
133        &self,
134        relative_path: &BStr,
135        is_dir: Option<bool>,
136        case: Case,
137    ) -> Option<(usize, usize, usize)> {
138        let groups = self.match_groups();
139        groups.iter().enumerate().rev().find_map(|(gidx, group)| {
140            let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
141            group
142                .patterns
143                .iter()
144                .enumerate()
145                .rev()
146                .find_map(|(plidx, pl)| {
147                    gix_ignore::search::pattern_idx_matching_relative_path(
148                        pl,
149                        relative_path,
150                        basename_pos,
151                        is_dir,
152                        case,
153                    )
154                    .map(|idx| (plidx, idx))
155                })
156                .map(|(plidx, pidx)| (gidx, plidx, pidx))
157        })
158    }
159
160    #[allow(clippy::too_many_arguments)]
161    pub(crate) fn push_directory(
162        &mut self,
163        root: &Path,
164        dir: &Path,
165        rela_dir: &BStr,
166        buf: &mut Vec<u8>,
167        id_mappings: &[PathIdMapping],
168        objects: &dyn gix_object::Find,
169        case: Case,
170        stats: &mut Statistics,
171    ) -> std::io::Result<()> {
172        self.matched_directory_patterns_stack
173            .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case));
174
175        let ignore_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore");
176        let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref()));
177        match self.source {
178            Source::IdMapping => {
179                match ignore_file_in_index {
180                    Ok(idx) => {
181                        let ignore_blob = objects
182                            .find_blob(&id_mappings[idx].1, buf)
183                            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
184                        let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
185                        self.stack
186                            .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));
187                        stats.patterns_buffers += 1;
188                    }
189                    Err(_) => {
190                        // Need one stack level per component so push and pop matches.
191                        self.stack.patterns.push(Default::default());
192                    }
193                }
194            }
195            Source::WorktreeThenIdMappingIfNotSkipped => {
196                let follow_symlinks = ignore_file_in_index.is_err();
197                let added = gix_glob::search::add_patterns_file(
198                    &mut self.stack.patterns,
199                    dir.join(".gitignore"),
200                    follow_symlinks,
201                    Some(root),
202                    buf,
203                )?;
204                stats.pattern_files += usize::from(added);
205                stats.tried_pattern_files += 1;
206                if !added {
207                    match ignore_file_in_index {
208                        Ok(idx) => {
209                            let ignore_blob = objects
210                                .find_blob(&id_mappings[idx].1, buf)
211                                .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
212                            let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned());
213                            self.stack
214                                .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")));
215                            stats.patterns_buffers += 1;
216                        }
217                        Err(_) => {
218                            // Need one stack level per component so push and pop matches.
219                            self.stack.patterns.push(Default::default());
220                        }
221                    }
222                }
223            }
224        }
225        Ok(())
226    }
227}