##// END OF EJS Templates
rust: implement `From<SparseConfigWarning>` for `HgError`...
Raphaël Gomès -
r52936:ae1ab6d7 default
parent child Browse files
Show More
@@ -1,115 +1,115
1 1 use std::path::Path;
2 2
3 3 use crate::{
4 4 errors::HgError,
5 5 exit_codes,
6 6 filepatterns::parse_pattern_file_contents,
7 7 matchers::{
8 8 AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
9 9 NeverMatcher,
10 10 },
11 11 repo::Repo,
12 12 requirements::NARROW_REQUIREMENT,
13 13 sparse::{self, SparseConfigError, SparseWarning},
14 14 };
15 15
16 16 /// The file in .hg/store/ that indicates which paths exit in the store
17 17 const FILENAME: &str = "narrowspec";
18 18 /// The file in .hg/ that indicates which paths exit in the dirstate
19 19 const DIRSTATE_FILENAME: &str = "narrowspec.dirstate";
20 20
21 21 /// Pattern prefixes that are allowed in narrow patterns. This list MUST
22 22 /// only contain patterns that are fast and safe to evaluate. Keep in mind
23 23 /// that patterns are supplied by clients and executed on remote servers
24 24 /// as part of wire protocol commands. That means that changes to this
25 25 /// data structure influence the wire protocol and should not be taken
26 26 /// lightly - especially removals.
27 const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"];
27 pub const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"];
28 28
29 29 /// Return the matcher for the current narrow spec, and all configuration
30 30 /// warnings to display.
31 31 pub fn matcher(
32 32 repo: &Repo,
33 33 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
34 34 let mut warnings = vec![];
35 35 if !repo.requirements().contains(NARROW_REQUIREMENT) {
36 36 return Ok((Box::new(AlwaysMatcher), warnings));
37 37 }
38 38 // Treat "narrowspec does not exist" the same as "narrowspec file exists
39 39 // and is empty".
40 40 let store_spec = repo.store_vfs().try_read(FILENAME)?.unwrap_or_default();
41 41 let working_copy_spec = repo
42 42 .hg_vfs()
43 43 .try_read(DIRSTATE_FILENAME)?
44 44 .unwrap_or_default();
45 45 if store_spec != working_copy_spec {
46 46 return Err(HgError::abort(
47 47 "abort: working copy's narrowspec is stale",
48 48 exit_codes::STATE_ERROR,
49 49 Some("run 'hg tracked --update-working-copy'".into()),
50 50 )
51 51 .into());
52 52 }
53 53
54 54 let config = sparse::parse_config(
55 55 &store_spec,
56 56 sparse::SparseConfigContext::Narrow,
57 57 )?;
58 58
59 59 warnings.extend(config.warnings);
60 60
61 61 if !config.profiles.is_empty() {
62 62 // TODO (from Python impl) maybe do something with profiles?
63 63 return Err(SparseConfigError::IncludesInNarrow);
64 64 }
65 65 validate_patterns(&config.includes)?;
66 66 validate_patterns(&config.excludes)?;
67 67
68 68 if config.includes.is_empty() {
69 69 return Ok((Box::new(NeverMatcher), warnings));
70 70 }
71 71
72 72 let (patterns, subwarnings) = parse_pattern_file_contents(
73 73 &config.includes,
74 74 Path::new(""),
75 75 None,
76 76 false,
77 77 true,
78 78 )?;
79 79 warnings.extend(subwarnings.into_iter().map(From::from));
80 80
81 81 let mut m: Box<dyn Matcher + Sync> =
82 82 Box::new(IncludeMatcher::new(patterns)?);
83 83
84 84 let (patterns, subwarnings) = parse_pattern_file_contents(
85 85 &config.excludes,
86 86 Path::new(""),
87 87 None,
88 88 false,
89 89 true,
90 90 )?;
91 91 if !patterns.is_empty() {
92 92 warnings.extend(subwarnings.into_iter().map(From::from));
93 93 let exclude_matcher = Box::new(IncludeMatcher::new(patterns)?);
94 94 m = Box::new(DifferenceMatcher::new(m, exclude_matcher));
95 95 }
96 96
97 97 Ok((m, warnings))
98 98 }
99 99
100 100 fn validate_patterns(patterns: &[u8]) -> Result<(), SparseConfigError> {
101 101 for pattern in patterns.split(|c| *c == b'\n') {
102 102 if pattern.is_empty() {
103 103 continue;
104 104 }
105 105 for prefix in VALID_PREFIXES.iter() {
106 106 if pattern.starts_with(prefix.as_bytes()) {
107 107 return Ok(());
108 108 }
109 109 }
110 110 return Err(SparseConfigError::InvalidNarrowPrefix(
111 111 pattern.to_owned(),
112 112 ));
113 113 }
114 114 Ok(())
115 115 }
@@ -1,341 +1,405
1 use std::{collections::HashSet, path::Path};
1 use std::{collections::HashSet, fmt::Display, path::Path};
2 2
3 use format_bytes::{write_bytes, DisplayBytes};
3 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
4 4
5 5 use crate::{
6 6 errors::HgError,
7 exit_codes::STATE_ERROR,
7 8 filepatterns::parse_pattern_file_contents,
8 9 matchers::{
9 10 AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
10 11 UnionMatcher,
11 12 },
13 narrow::VALID_PREFIXES,
12 14 operations::cat,
13 15 repo::Repo,
14 16 requirements::SPARSE_REQUIREMENT,
15 17 utils::{hg_path::HgPath, SliceExt},
16 18 IgnorePattern, PatternError, PatternFileWarning, PatternSyntax, Revision,
17 19 NULL_REVISION,
18 20 };
19 21
20 22 /// Command which is triggering the config read
21 23 #[derive(Copy, Clone, Debug)]
22 24 pub enum SparseConfigContext {
23 25 Sparse,
24 26 Narrow,
25 27 }
26 28
27 29 impl DisplayBytes for SparseConfigContext {
28 30 fn display_bytes(
29 31 &self,
30 32 output: &mut dyn std::io::Write,
31 33 ) -> std::io::Result<()> {
32 34 match self {
33 35 SparseConfigContext::Sparse => write_bytes!(output, b"sparse"),
34 36 SparseConfigContext::Narrow => write_bytes!(output, b"narrow"),
35 37 }
36 38 }
37 39 }
38 40
41 impl Display for SparseConfigContext {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 SparseConfigContext::Sparse => write!(f, "sparse"),
45 SparseConfigContext::Narrow => write!(f, "narrow"),
46 }
47 }
48 }
49
39 50 /// Possible warnings when reading sparse configuration
40 51 #[derive(Debug, derive_more::From)]
41 52 pub enum SparseWarning {
42 53 /// Warns about improper paths that start with "/"
43 54 RootWarning {
44 55 context: SparseConfigContext,
45 56 line: Vec<u8>,
46 57 },
47 58 /// Warns about a profile missing from the given changelog revision
48 59 ProfileNotFound { profile: Vec<u8>, rev: Revision },
49 60 #[from]
50 61 Pattern(PatternFileWarning),
51 62 }
52 63
53 64 /// Parsed sparse config
54 65 #[derive(Debug, Default)]
55 66 pub struct SparseConfig {
56 67 // Line-separated
57 68 pub(crate) includes: Vec<u8>,
58 69 // Line-separated
59 70 pub(crate) excludes: Vec<u8>,
60 71 pub(crate) profiles: HashSet<Vec<u8>>,
61 72 pub(crate) warnings: Vec<SparseWarning>,
62 73 }
63 74
64 75 /// All possible errors when reading sparse/narrow config
65 76 #[derive(Debug, derive_more::From)]
66 77 pub enum SparseConfigError {
67 78 IncludesAfterExcludes {
68 79 context: SparseConfigContext,
69 80 },
70 81 EntryOutsideSection {
71 82 context: SparseConfigContext,
72 83 line: Vec<u8>,
73 84 },
74 85 /// Narrow config does not support '%include' directives
75 86 IncludesInNarrow,
76 87 /// An invalid pattern prefix was given to the narrow spec. Includes the
77 88 /// entire pattern for context.
78 89 InvalidNarrowPrefix(Vec<u8>),
79 90 #[from]
80 91 HgError(HgError),
81 92 #[from]
82 93 PatternError(PatternError),
83 94 }
84 95
96 impl From<SparseConfigError> for HgError {
97 fn from(value: SparseConfigError) -> Self {
98 match value {
99 SparseConfigError::IncludesAfterExcludes { context } => {
100 HgError::Abort {
101 message: format!(
102 "{} config cannot have includes after excludes",
103 context,
104 ),
105 detailed_exit_code: STATE_ERROR,
106 hint: None,
107 }
108 }
109 SparseConfigError::EntryOutsideSection { context, line } => {
110 HgError::Abort {
111 message: format!(
112 "{} config entry outside of section: {}",
113 context,
114 String::from_utf8_lossy(&line)
115 ),
116 detailed_exit_code: STATE_ERROR,
117 hint: None,
118 }
119 }
120 SparseConfigError::IncludesInNarrow => HgError::Abort {
121 message: "including other spec files using '%include' is not \
122 supported in narrowspec"
123 .to_string(),
124 detailed_exit_code: STATE_ERROR,
125 hint: None,
126 },
127 SparseConfigError::InvalidNarrowPrefix(vec) => HgError::Abort {
128 message: String::from_utf8_lossy(&format_bytes!(
129 b"invalid prefix on narrow pattern: {}",
130 vec
131 ))
132 .to_string(),
133 detailed_exit_code: STATE_ERROR,
134 hint: Some(format!(
135 "narrow patterns must begin with one of the following: {}",
136 VALID_PREFIXES.join(", ")
137 )),
138 },
139 SparseConfigError::HgError(hg_error) => hg_error,
140 SparseConfigError::PatternError(pattern_error) => HgError::Abort {
141 message: pattern_error.to_string(),
142 detailed_exit_code: STATE_ERROR,
143 hint: None,
144 },
145 }
146 }
147 }
148
85 149 /// Parse sparse config file content.
86 150 pub(crate) fn parse_config(
87 151 raw: &[u8],
88 152 context: SparseConfigContext,
89 153 ) -> Result<SparseConfig, SparseConfigError> {
90 154 let mut includes = vec![];
91 155 let mut excludes = vec![];
92 156 let mut profiles = HashSet::new();
93 157 let mut warnings = vec![];
94 158
95 159 #[derive(PartialEq, Eq)]
96 160 enum Current {
97 161 Includes,
98 162 Excludes,
99 163 None,
100 164 }
101 165
102 166 let mut current = Current::None;
103 167 let mut in_section = false;
104 168
105 169 for line in raw.split(|c| *c == b'\n') {
106 170 let line = line.trim();
107 171 if line.is_empty() || line[0] == b'#' {
108 172 // empty or comment line, skip
109 173 continue;
110 174 }
111 175 if line.starts_with(b"%include ") {
112 176 let profile = line[b"%include ".len()..].trim();
113 177 if !profile.is_empty() {
114 178 profiles.insert(profile.into());
115 179 }
116 180 } else if line == b"[include]" {
117 181 if in_section && current == Current::Includes {
118 182 return Err(SparseConfigError::IncludesAfterExcludes {
119 183 context,
120 184 });
121 185 }
122 186 in_section = true;
123 187 current = Current::Includes;
124 188 continue;
125 189 } else if line == b"[exclude]" {
126 190 in_section = true;
127 191 current = Current::Excludes;
128 192 } else {
129 193 if current == Current::None {
130 194 return Err(SparseConfigError::EntryOutsideSection {
131 195 context,
132 196 line: line.into(),
133 197 });
134 198 }
135 199 if line.trim().starts_with(b"/") {
136 200 warnings.push(SparseWarning::RootWarning {
137 201 context,
138 202 line: line.into(),
139 203 });
140 204 continue;
141 205 }
142 206 match current {
143 207 Current::Includes => {
144 208 includes.push(b'\n');
145 209 includes.extend(line.iter());
146 210 }
147 211 Current::Excludes => {
148 212 excludes.push(b'\n');
149 213 excludes.extend(line.iter());
150 214 }
151 215 Current::None => unreachable!(),
152 216 }
153 217 }
154 218 }
155 219
156 220 Ok(SparseConfig {
157 221 includes,
158 222 excludes,
159 223 profiles,
160 224 warnings,
161 225 })
162 226 }
163 227
164 228 fn read_temporary_includes(
165 229 repo: &Repo,
166 230 ) -> Result<Vec<Vec<u8>>, SparseConfigError> {
167 231 let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or_default();
168 232 if raw.is_empty() {
169 233 return Ok(vec![]);
170 234 }
171 235 Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect())
172 236 }
173 237
174 238 /// Obtain sparse checkout patterns for the given revision
175 239 fn patterns_for_rev(
176 240 repo: &Repo,
177 241 rev: Revision,
178 242 ) -> Result<Option<SparseConfig>, SparseConfigError> {
179 243 if !repo.has_sparse() {
180 244 return Ok(None);
181 245 }
182 246 let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or_default();
183 247
184 248 if raw.is_empty() {
185 249 return Ok(None);
186 250 }
187 251
188 252 let mut config = parse_config(&raw, SparseConfigContext::Sparse)?;
189 253
190 254 if !config.profiles.is_empty() {
191 255 let mut profiles: Vec<Vec<u8>> = config.profiles.into_iter().collect();
192 256 let mut visited = HashSet::new();
193 257
194 258 while let Some(profile) = profiles.pop() {
195 259 if visited.contains(&profile) {
196 260 continue;
197 261 }
198 262 visited.insert(profile.to_owned());
199 263
200 264 let output =
201 265 cat(repo, &rev.to_string(), vec![HgPath::new(&profile)])
202 266 .map_err(|_| {
203 267 HgError::corrupted(
204 268 "dirstate points to non-existent parent node"
205 269 .to_string(),
206 270 )
207 271 })?;
208 272 if output.results.is_empty() {
209 273 config.warnings.push(SparseWarning::ProfileNotFound {
210 274 profile: profile.to_owned(),
211 275 rev,
212 276 })
213 277 }
214 278
215 279 let subconfig = parse_config(
216 280 &output.results[0].1,
217 281 SparseConfigContext::Sparse,
218 282 )?;
219 283 if !subconfig.includes.is_empty() {
220 284 config.includes.push(b'\n');
221 285 config.includes.extend(&subconfig.includes);
222 286 }
223 287 if !subconfig.includes.is_empty() {
224 288 config.includes.push(b'\n');
225 289 config.excludes.extend(&subconfig.excludes);
226 290 }
227 291 config.warnings.extend(subconfig.warnings.into_iter());
228 292 profiles.extend(subconfig.profiles.into_iter());
229 293 }
230 294
231 295 config.profiles = visited;
232 296 }
233 297
234 298 if !config.includes.is_empty() {
235 299 config.includes.extend(b"\n.hg*");
236 300 }
237 301
238 302 Ok(Some(config))
239 303 }
240 304
241 305 /// Obtain a matcher for sparse working directories.
242 306 pub fn matcher(
243 307 repo: &Repo,
244 308 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
245 309 let mut warnings = vec![];
246 310 if !repo.requirements().contains(SPARSE_REQUIREMENT) {
247 311 return Ok((Box::new(AlwaysMatcher), warnings));
248 312 }
249 313
250 314 let parents = repo.dirstate_parents()?;
251 315 let mut revs = vec![];
252 316 let p1_rev =
253 317 repo.changelog()?
254 318 .rev_from_node(parents.p1.into())
255 319 .map_err(|_| {
256 320 HgError::corrupted(
257 321 "dirstate points to non-existent parent node".to_string(),
258 322 )
259 323 })?;
260 324 if p1_rev != NULL_REVISION {
261 325 revs.push(p1_rev)
262 326 }
263 327 let p2_rev =
264 328 repo.changelog()?
265 329 .rev_from_node(parents.p2.into())
266 330 .map_err(|_| {
267 331 HgError::corrupted(
268 332 "dirstate points to non-existent parent node".to_string(),
269 333 )
270 334 })?;
271 335 if p2_rev != NULL_REVISION {
272 336 revs.push(p2_rev)
273 337 }
274 338 let mut matchers = vec![];
275 339
276 340 for rev in revs.iter() {
277 341 let config = patterns_for_rev(repo, *rev);
278 342 if let Ok(Some(config)) = config {
279 343 warnings.extend(config.warnings);
280 344 let mut m: Box<dyn Matcher + Sync> = Box::new(AlwaysMatcher);
281 345 if !config.includes.is_empty() {
282 346 let (patterns, subwarnings) = parse_pattern_file_contents(
283 347 &config.includes,
284 348 Path::new(""),
285 349 Some(PatternSyntax::Glob),
286 350 false,
287 351 false,
288 352 )?;
289 353 warnings.extend(subwarnings.into_iter().map(From::from));
290 354 m = Box::new(IncludeMatcher::new(patterns)?);
291 355 }
292 356 if !config.excludes.is_empty() {
293 357 let (patterns, subwarnings) = parse_pattern_file_contents(
294 358 &config.excludes,
295 359 Path::new(""),
296 360 Some(PatternSyntax::Glob),
297 361 false,
298 362 false,
299 363 )?;
300 364 warnings.extend(subwarnings.into_iter().map(From::from));
301 365 m = Box::new(DifferenceMatcher::new(
302 366 m,
303 367 Box::new(IncludeMatcher::new(patterns)?),
304 368 ));
305 369 }
306 370 matchers.push(m);
307 371 }
308 372 }
309 373 let result: Box<dyn Matcher + Sync> = match matchers.len() {
310 374 0 => Box::new(AlwaysMatcher),
311 375 1 => matchers.pop().expect("1 is equal to 0"),
312 376 _ => Box::new(UnionMatcher::new(matchers)),
313 377 };
314 378
315 379 let matcher =
316 380 force_include_matcher(result, &read_temporary_includes(repo)?)?;
317 381 Ok((matcher, warnings))
318 382 }
319 383
320 384 /// Returns a matcher that returns true for any of the forced includes before
321 385 /// testing against the actual matcher
322 386 fn force_include_matcher(
323 387 result: Box<dyn Matcher + Sync>,
324 388 temp_includes: &[Vec<u8>],
325 389 ) -> Result<Box<dyn Matcher + Sync>, PatternError> {
326 390 if temp_includes.is_empty() {
327 391 return Ok(result);
328 392 }
329 393 let forced_include_matcher = IncludeMatcher::new(
330 394 temp_includes
331 395 .iter()
332 396 .map(|include| {
333 397 IgnorePattern::new(PatternSyntax::Path, include, Path::new(""))
334 398 })
335 399 .collect(),
336 400 )?;
337 401 Ok(Box::new(UnionMatcher::new(vec![
338 402 Box::new(forced_include_matcher),
339 403 result,
340 404 ])))
341 405 }
General Comments 0
You need to be logged in to leave comments. Login now