Mercurial > hg
annotate rust/hg-core/src/sparse.rs @ 51470:406b413e3cf2 stable
rust-filepatterns: export glob_to_re function
Making this function public should not risk freezing the internal API,
and it can be useful for all downstream code that needs to perform
glob matching against byte strings, such as RHGitaly where it will
be useful to match on branches and tags.
author | Georges Racinet <georges.racinet@octobus.net> |
---|---|
date | Mon, 11 Mar 2024 13:23:18 +0100 |
parents | 796b5d6693a4 |
children | ae1ab6d71f4a |
rev | line source |
---|---|
49485 | 1 use std::{collections::HashSet, path::Path}; |
2 | |
3 use format_bytes::{write_bytes, DisplayBytes}; | |
4 | |
5 use crate::{ | |
6 errors::HgError, | |
7 filepatterns::parse_pattern_file_contents, | |
8 matchers::{ | |
9 AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher, | |
10 UnionMatcher, | |
11 }, | |
12 operations::cat, | |
13 repo::Repo, | |
14 requirements::SPARSE_REQUIREMENT, | |
15 utils::{hg_path::HgPath, SliceExt}, | |
16 IgnorePattern, PatternError, PatternFileWarning, PatternSyntax, Revision, | |
17 NULL_REVISION, | |
18 }; | |
19 | |
20 /// Command which is triggering the config read | |
21 #[derive(Copy, Clone, Debug)] | |
22 pub enum SparseConfigContext { | |
23 Sparse, | |
24 Narrow, | |
25 } | |
26 | |
27 impl DisplayBytes for SparseConfigContext { | |
28 fn display_bytes( | |
29 &self, | |
30 output: &mut dyn std::io::Write, | |
31 ) -> std::io::Result<()> { | |
32 match self { | |
33 SparseConfigContext::Sparse => write_bytes!(output, b"sparse"), | |
34 SparseConfigContext::Narrow => write_bytes!(output, b"narrow"), | |
35 } | |
36 } | |
37 } | |
38 | |
39 /// Possible warnings when reading sparse configuration | |
40 #[derive(Debug, derive_more::From)] | |
41 pub enum SparseWarning { | |
42 /// Warns about improper paths that start with "/" | |
43 RootWarning { | |
44 context: SparseConfigContext, | |
45 line: Vec<u8>, | |
46 }, | |
47 /// Warns about a profile missing from the given changelog revision | |
48 ProfileNotFound { profile: Vec<u8>, rev: Revision }, | |
49 #[from] | |
50 Pattern(PatternFileWarning), | |
51 } | |
52 | |
53 /// Parsed sparse config | |
54 #[derive(Debug, Default)] | |
55 pub struct SparseConfig { | |
56 // Line-separated | |
49489
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
57 pub(crate) includes: Vec<u8>, |
49485 | 58 // Line-separated |
49489
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
59 pub(crate) excludes: Vec<u8>, |
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
60 pub(crate) profiles: HashSet<Vec<u8>>, |
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
61 pub(crate) warnings: Vec<SparseWarning>, |
49485 | 62 } |
63 | |
49489
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
64 /// All possible errors when reading sparse/narrow config |
49485 | 65 #[derive(Debug, derive_more::From)] |
66 pub enum SparseConfigError { | |
67 IncludesAfterExcludes { | |
68 context: SparseConfigContext, | |
69 }, | |
70 EntryOutsideSection { | |
71 context: SparseConfigContext, | |
72 line: Vec<u8>, | |
73 }, | |
49489
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
74 /// Narrow config does not support '%include' directives |
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
75 IncludesInNarrow, |
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
76 /// An invalid pattern prefix was given to the narrow spec. Includes the |
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
77 /// entire pattern for context. |
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
78 InvalidNarrowPrefix(Vec<u8>), |
49485 | 79 #[from] |
80 HgError(HgError), | |
81 #[from] | |
82 PatternError(PatternError), | |
83 } | |
84 | |
85 /// Parse sparse config file content. | |
49489
7c93e38a0bbd
rhg-status: add support for narrow clones
Raphaël Gomès <rgomes@octobus.net>
parents:
49485
diff
changeset
|
86 pub(crate) fn parse_config( |
49485 | 87 raw: &[u8], |
88 context: SparseConfigContext, | |
89 ) -> Result<SparseConfig, SparseConfigError> { | |
90 let mut includes = vec![]; | |
91 let mut excludes = vec![]; | |
92 let mut profiles = HashSet::new(); | |
93 let mut warnings = vec![]; | |
94 | |
95 #[derive(PartialEq, Eq)] | |
96 enum Current { | |
97 Includes, | |
98 Excludes, | |
99 None, | |
49517
52464a20add0
rhg: parallellize computation of [unsure_is_modified]
Arseniy Alekseyev <aalekseyev@janestreet.com>
parents:
49489
diff
changeset
|
100 } |
49485 | 101 |
102 let mut current = Current::None; | |
103 let mut in_section = false; | |
104 | |
105 for line in raw.split(|c| *c == b'\n') { | |
106 let line = line.trim(); | |
107 if line.is_empty() || line[0] == b'#' { | |
108 // empty or comment line, skip | |
109 continue; | |
110 } | |
111 if line.starts_with(b"%include ") { | |
112 let profile = line[b"%include ".len()..].trim(); | |
113 if !profile.is_empty() { | |
114 profiles.insert(profile.into()); | |
115 } | |
116 } else if line == b"[include]" { | |
117 if in_section && current == Current::Includes { | |
118 return Err(SparseConfigError::IncludesAfterExcludes { | |
119 context, | |
120 }); | |
121 } | |
122 in_section = true; | |
123 current = Current::Includes; | |
124 continue; | |
125 } else if line == b"[exclude]" { | |
126 in_section = true; | |
127 current = Current::Excludes; | |
128 } else { | |
129 if current == Current::None { | |
130 return Err(SparseConfigError::EntryOutsideSection { | |
131 context, | |
132 line: line.into(), | |
133 }); | |
134 } | |
135 if line.trim().starts_with(b"/") { | |
136 warnings.push(SparseWarning::RootWarning { | |
137 context, | |
138 line: line.into(), | |
139 }); | |
140 continue; | |
141 } | |
142 match current { | |
143 Current::Includes => { | |
144 includes.push(b'\n'); | |
145 includes.extend(line.iter()); | |
146 } | |
147 Current::Excludes => { | |
148 excludes.push(b'\n'); | |
149 excludes.extend(line.iter()); | |
150 } | |
151 Current::None => unreachable!(), | |
152 } | |
153 } | |
154 } | |
155 | |
156 Ok(SparseConfig { | |
157 includes, | |
158 excludes, | |
159 profiles, | |
160 warnings, | |
161 }) | |
162 } | |
163 | |
164 fn read_temporary_includes( | |
165 repo: &Repo, | |
166 ) -> Result<Vec<Vec<u8>>, SparseConfigError> { | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
167 let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or_default(); |
49485 | 168 if raw.is_empty() { |
169 return Ok(vec![]); | |
170 } | |
171 Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect()) | |
172 } | |
173 | |
174 /// Obtain sparse checkout patterns for the given revision | |
175 fn patterns_for_rev( | |
176 repo: &Repo, | |
177 rev: Revision, | |
178 ) -> Result<Option<SparseConfig>, SparseConfigError> { | |
179 if !repo.has_sparse() { | |
180 return Ok(None); | |
181 } | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
182 let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or_default(); |
49485 | 183 |
184 if raw.is_empty() { | |
185 return Ok(None); | |
186 } | |
187 | |
188 let mut config = parse_config(&raw, SparseConfigContext::Sparse)?; | |
189 | |
190 if !config.profiles.is_empty() { | |
191 let mut profiles: Vec<Vec<u8>> = config.profiles.into_iter().collect(); | |
192 let mut visited = HashSet::new(); | |
193 | |
194 while let Some(profile) = profiles.pop() { | |
195 if visited.contains(&profile) { | |
196 continue; | |
197 } | |
198 visited.insert(profile.to_owned()); | |
199 | |
200 let output = | |
201 cat(repo, &rev.to_string(), vec![HgPath::new(&profile)]) | |
202 .map_err(|_| { | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
203 HgError::corrupted( |
49485 | 204 "dirstate points to non-existent parent node" |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
205 .to_string(), |
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
206 ) |
49485 | 207 })?; |
208 if output.results.is_empty() { | |
209 config.warnings.push(SparseWarning::ProfileNotFound { | |
210 profile: profile.to_owned(), | |
211 rev, | |
212 }) | |
213 } | |
214 | |
215 let subconfig = parse_config( | |
216 &output.results[0].1, | |
217 SparseConfigContext::Sparse, | |
218 )?; | |
219 if !subconfig.includes.is_empty() { | |
220 config.includes.push(b'\n'); | |
221 config.includes.extend(&subconfig.includes); | |
222 } | |
223 if !subconfig.includes.is_empty() { | |
224 config.includes.push(b'\n'); | |
225 config.excludes.extend(&subconfig.excludes); | |
226 } | |
227 config.warnings.extend(subconfig.warnings.into_iter()); | |
228 profiles.extend(subconfig.profiles.into_iter()); | |
229 } | |
230 | |
231 config.profiles = visited; | |
232 } | |
233 | |
234 if !config.includes.is_empty() { | |
235 config.includes.extend(b"\n.hg*"); | |
236 } | |
237 | |
238 Ok(Some(config)) | |
239 } | |
240 | |
241 /// Obtain a matcher for sparse working directories. | |
242 pub fn matcher( | |
243 repo: &Repo, | |
244 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> { | |
245 let mut warnings = vec![]; | |
246 if !repo.requirements().contains(SPARSE_REQUIREMENT) { | |
247 return Ok((Box::new(AlwaysMatcher), warnings)); | |
248 } | |
249 | |
250 let parents = repo.dirstate_parents()?; | |
251 let mut revs = vec![]; | |
252 let p1_rev = | |
253 repo.changelog()? | |
254 .rev_from_node(parents.p1.into()) | |
255 .map_err(|_| { | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
256 HgError::corrupted( |
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
257 "dirstate points to non-existent parent node".to_string(), |
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
258 ) |
49485 | 259 })?; |
260 if p1_rev != NULL_REVISION { | |
261 revs.push(p1_rev) | |
262 } | |
263 let p2_rev = | |
264 repo.changelog()? | |
265 .rev_from_node(parents.p2.into()) | |
266 .map_err(|_| { | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
267 HgError::corrupted( |
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
268 "dirstate points to non-existent parent node".to_string(), |
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
269 ) |
49485 | 270 })?; |
271 if p2_rev != NULL_REVISION { | |
272 revs.push(p2_rev) | |
273 } | |
274 let mut matchers = vec![]; | |
275 | |
276 for rev in revs.iter() { | |
277 let config = patterns_for_rev(repo, *rev); | |
278 if let Ok(Some(config)) = config { | |
279 warnings.extend(config.warnings); | |
280 let mut m: Box<dyn Matcher + Sync> = Box::new(AlwaysMatcher); | |
281 if !config.includes.is_empty() { | |
282 let (patterns, subwarnings) = parse_pattern_file_contents( | |
283 &config.includes, | |
284 Path::new(""), | |
50857
796b5d6693a4
rust: simplify pattern file parsing
Spencer Baugh <sbaugh@janestreet.com>
parents:
50821
diff
changeset
|
285 Some(PatternSyntax::Glob), |
796b5d6693a4
rust: simplify pattern file parsing
Spencer Baugh <sbaugh@janestreet.com>
parents:
50821
diff
changeset
|
286 false, |
49485 | 287 false, |
288 )?; | |
289 warnings.extend(subwarnings.into_iter().map(From::from)); | |
290 m = Box::new(IncludeMatcher::new(patterns)?); | |
291 } | |
292 if !config.excludes.is_empty() { | |
293 let (patterns, subwarnings) = parse_pattern_file_contents( | |
294 &config.excludes, | |
295 Path::new(""), | |
50857
796b5d6693a4
rust: simplify pattern file parsing
Spencer Baugh <sbaugh@janestreet.com>
parents:
50821
diff
changeset
|
296 Some(PatternSyntax::Glob), |
796b5d6693a4
rust: simplify pattern file parsing
Spencer Baugh <sbaugh@janestreet.com>
parents:
50821
diff
changeset
|
297 false, |
49485 | 298 false, |
299 )?; | |
300 warnings.extend(subwarnings.into_iter().map(From::from)); | |
301 m = Box::new(DifferenceMatcher::new( | |
302 m, | |
303 Box::new(IncludeMatcher::new(patterns)?), | |
304 )); | |
305 } | |
306 matchers.push(m); | |
307 } | |
308 } | |
309 let result: Box<dyn Matcher + Sync> = match matchers.len() { | |
310 0 => Box::new(AlwaysMatcher), | |
311 1 => matchers.pop().expect("1 is equal to 0"), | |
312 _ => Box::new(UnionMatcher::new(matchers)), | |
313 }; | |
314 | |
315 let matcher = | |
316 force_include_matcher(result, &read_temporary_includes(repo)?)?; | |
317 Ok((matcher, warnings)) | |
318 } | |
319 | |
320 /// Returns a matcher that returns true for any of the forced includes before | |
321 /// testing against the actual matcher | |
322 fn force_include_matcher( | |
323 result: Box<dyn Matcher + Sync>, | |
324 temp_includes: &[Vec<u8>], | |
325 ) -> Result<Box<dyn Matcher + Sync>, PatternError> { | |
326 if temp_includes.is_empty() { | |
327 return Ok(result); | |
328 } | |
329 let forced_include_matcher = IncludeMatcher::new( | |
330 temp_includes | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Raphaël Gomès <rgomes@octobus.net>
parents:
49517
diff
changeset
|
331 .iter() |
49485 | 332 .map(|include| { |
333 IgnorePattern::new(PatternSyntax::Path, include, Path::new("")) | |
334 }) | |
335 .collect(), | |
336 )?; | |
337 Ok(Box::new(UnionMatcher::new(vec![ | |
338 Box::new(forced_include_matcher), | |
339 result, | |
340 ]))) | |
341 } |