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