Mercurial > hg
annotate rust/hg-core/src/sparse.rs @ 50985:363620b934aa stable
revlog: fix a bug where NULL_NODE failed to be resolved to NULL_REV
The problem is that nodemap already takes care about NULL_NODE
resolution (in `validate_candidate` in `nodemap.rs`), so the special
handling in `rev_from_node` is unnecessary and incorrect.
author | Arseniy Alekseyev <aalekseyev@janestreet.com> |
---|---|
date | Wed, 13 Sep 2023 18:28:51 +0100 |
parents | 28c0fcff24e5 |
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(""), | |
50821
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(""), | |
50821
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 } |