Mercurial > public > mercurial-scm > hg
annotate rust/hg-core/src/sparse.rs @ 50818:28c0fcff24e5 stable
rhg: fix the bug where sparse config is interpreted as relglob instead of glob
relglob apparently (in contrast with relpath) matches everywhere in the tree,
whereas glob only matches at the root.
The python version interprets these patterns as "glob" (see
"normalize(include, b'glob', ...)" in match.py)
author | Arseniy Alekseyev <aalekseyev@janestreet.com> |
---|---|
date | Thu, 10 Aug 2023 19:00:19 +0100 |
parents | e98fd81bb151 |
children | 796b5d6693a4 |
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(""), | |
50818
28c0fcff24e5
rhg: fix the bug where sparse config is interpreted as relglob instead of glob
Arseniy Alekseyev <aalekseyev@janestreet.com>
parents:
49930
diff
changeset
|
285 Some(b"glob:".as_ref()), |
49485 | 286 false, |
287 )?; | |
288 warnings.extend(subwarnings.into_iter().map(From::from)); | |
289 m = Box::new(IncludeMatcher::new(patterns)?); | |
290 } | |
291 if !config.excludes.is_empty() { | |
292 let (patterns, subwarnings) = parse_pattern_file_contents( | |
293 &config.excludes, | |
294 Path::new(""), | |
50818
28c0fcff24e5
rhg: fix the bug where sparse config is interpreted as relglob instead of glob
Arseniy Alekseyev <aalekseyev@janestreet.com>
parents:
49930
diff
changeset
|
295 Some(b"glob:".as_ref()), |
49485 | 296 false, |
297 )?; | |
298 warnings.extend(subwarnings.into_iter().map(From::from)); | |
299 m = Box::new(DifferenceMatcher::new( | |
300 m, | |
301 Box::new(IncludeMatcher::new(patterns)?), | |
302 )); | |
303 } | |
304 matchers.push(m); | |
305 } | |
306 } | |
307 let result: Box<dyn Matcher + Sync> = match matchers.len() { | |
308 0 => Box::new(AlwaysMatcher), | |
309 1 => matchers.pop().expect("1 is equal to 0"), | |
310 _ => Box::new(UnionMatcher::new(matchers)), | |
311 }; | |
312 | |
313 let matcher = | |
314 force_include_matcher(result, &read_temporary_includes(repo)?)?; | |
315 Ok((matcher, warnings)) | |
316 } | |
317 | |
318 /// Returns a matcher that returns true for any of the forced includes before | |
319 /// testing against the actual matcher | |
320 fn force_include_matcher( | |
321 result: Box<dyn Matcher + Sync>, | |
322 temp_includes: &[Vec<u8>], | |
323 ) -> Result<Box<dyn Matcher + Sync>, PatternError> { | |
324 if temp_includes.is_empty() { | |
325 return Ok(result); | |
326 } | |
327 let forced_include_matcher = IncludeMatcher::new( | |
328 temp_includes | |
49930
e98fd81bb151
rust-clippy: fix most warnings in `hg-core`
Rapha?l Gom?s <rgomes@octobus.net>
parents:
49517
diff
changeset
|
329 .iter() |
49485 | 330 .map(|include| { |
331 IgnorePattern::new(PatternSyntax::Path, include, Path::new("")) | |
332 }) | |
333 .collect(), | |
334 )?; | |
335 Ok(Box::new(UnionMatcher::new(vec![ | |
336 Box::new(forced_include_matcher), | |
337 result, | |
338 ]))) | |
339 } |