|
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 |
|
57 includes: Vec<u8>, |
|
58 // Line-separated |
|
59 excludes: Vec<u8>, |
|
60 profiles: HashSet<Vec<u8>>, |
|
61 warnings: Vec<SparseWarning>, |
|
62 } |
|
63 |
|
64 /// All possible errors when reading sparse config |
|
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 }, |
|
74 #[from] |
|
75 HgError(HgError), |
|
76 #[from] |
|
77 PatternError(PatternError), |
|
78 } |
|
79 |
|
80 /// Parse sparse config file content. |
|
81 fn parse_config( |
|
82 raw: &[u8], |
|
83 context: SparseConfigContext, |
|
84 ) -> Result<SparseConfig, SparseConfigError> { |
|
85 let mut includes = vec![]; |
|
86 let mut excludes = vec![]; |
|
87 let mut profiles = HashSet::new(); |
|
88 let mut warnings = vec![]; |
|
89 |
|
90 #[derive(PartialEq, Eq)] |
|
91 enum Current { |
|
92 Includes, |
|
93 Excludes, |
|
94 None, |
|
95 }; |
|
96 |
|
97 let mut current = Current::None; |
|
98 let mut in_section = false; |
|
99 |
|
100 for line in raw.split(|c| *c == b'\n') { |
|
101 let line = line.trim(); |
|
102 if line.is_empty() || line[0] == b'#' { |
|
103 // empty or comment line, skip |
|
104 continue; |
|
105 } |
|
106 if line.starts_with(b"%include ") { |
|
107 let profile = line[b"%include ".len()..].trim(); |
|
108 if !profile.is_empty() { |
|
109 profiles.insert(profile.into()); |
|
110 } |
|
111 } else if line == b"[include]" { |
|
112 if in_section && current == Current::Includes { |
|
113 return Err(SparseConfigError::IncludesAfterExcludes { |
|
114 context, |
|
115 }); |
|
116 } |
|
117 in_section = true; |
|
118 current = Current::Includes; |
|
119 continue; |
|
120 } else if line == b"[exclude]" { |
|
121 in_section = true; |
|
122 current = Current::Excludes; |
|
123 } else { |
|
124 if current == Current::None { |
|
125 return Err(SparseConfigError::EntryOutsideSection { |
|
126 context, |
|
127 line: line.into(), |
|
128 }); |
|
129 } |
|
130 if line.trim().starts_with(b"/") { |
|
131 warnings.push(SparseWarning::RootWarning { |
|
132 context, |
|
133 line: line.into(), |
|
134 }); |
|
135 continue; |
|
136 } |
|
137 match current { |
|
138 Current::Includes => { |
|
139 includes.push(b'\n'); |
|
140 includes.extend(line.iter()); |
|
141 } |
|
142 Current::Excludes => { |
|
143 excludes.push(b'\n'); |
|
144 excludes.extend(line.iter()); |
|
145 } |
|
146 Current::None => unreachable!(), |
|
147 } |
|
148 } |
|
149 } |
|
150 |
|
151 Ok(SparseConfig { |
|
152 includes, |
|
153 excludes, |
|
154 profiles, |
|
155 warnings, |
|
156 }) |
|
157 } |
|
158 |
|
159 fn read_temporary_includes( |
|
160 repo: &Repo, |
|
161 ) -> Result<Vec<Vec<u8>>, SparseConfigError> { |
|
162 let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or(vec![]); |
|
163 if raw.is_empty() { |
|
164 return Ok(vec![]); |
|
165 } |
|
166 Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect()) |
|
167 } |
|
168 |
|
169 /// Obtain sparse checkout patterns for the given revision |
|
170 fn patterns_for_rev( |
|
171 repo: &Repo, |
|
172 rev: Revision, |
|
173 ) -> Result<Option<SparseConfig>, SparseConfigError> { |
|
174 if !repo.has_sparse() { |
|
175 return Ok(None); |
|
176 } |
|
177 let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or(vec![]); |
|
178 |
|
179 if raw.is_empty() { |
|
180 return Ok(None); |
|
181 } |
|
182 |
|
183 let mut config = parse_config(&raw, SparseConfigContext::Sparse)?; |
|
184 |
|
185 if !config.profiles.is_empty() { |
|
186 let mut profiles: Vec<Vec<u8>> = config.profiles.into_iter().collect(); |
|
187 let mut visited = HashSet::new(); |
|
188 |
|
189 while let Some(profile) = profiles.pop() { |
|
190 if visited.contains(&profile) { |
|
191 continue; |
|
192 } |
|
193 visited.insert(profile.to_owned()); |
|
194 |
|
195 let output = |
|
196 cat(repo, &rev.to_string(), vec![HgPath::new(&profile)]) |
|
197 .map_err(|_| { |
|
198 HgError::corrupted(format!( |
|
199 "dirstate points to non-existent parent node" |
|
200 )) |
|
201 })?; |
|
202 if output.results.is_empty() { |
|
203 config.warnings.push(SparseWarning::ProfileNotFound { |
|
204 profile: profile.to_owned(), |
|
205 rev, |
|
206 }) |
|
207 } |
|
208 |
|
209 let subconfig = parse_config( |
|
210 &output.results[0].1, |
|
211 SparseConfigContext::Sparse, |
|
212 )?; |
|
213 if !subconfig.includes.is_empty() { |
|
214 config.includes.push(b'\n'); |
|
215 config.includes.extend(&subconfig.includes); |
|
216 } |
|
217 if !subconfig.includes.is_empty() { |
|
218 config.includes.push(b'\n'); |
|
219 config.excludes.extend(&subconfig.excludes); |
|
220 } |
|
221 config.warnings.extend(subconfig.warnings.into_iter()); |
|
222 profiles.extend(subconfig.profiles.into_iter()); |
|
223 } |
|
224 |
|
225 config.profiles = visited; |
|
226 } |
|
227 |
|
228 if !config.includes.is_empty() { |
|
229 config.includes.extend(b"\n.hg*"); |
|
230 } |
|
231 |
|
232 Ok(Some(config)) |
|
233 } |
|
234 |
|
235 /// Obtain a matcher for sparse working directories. |
|
236 pub fn matcher( |
|
237 repo: &Repo, |
|
238 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> { |
|
239 let mut warnings = vec![]; |
|
240 if !repo.requirements().contains(SPARSE_REQUIREMENT) { |
|
241 return Ok((Box::new(AlwaysMatcher), warnings)); |
|
242 } |
|
243 |
|
244 let parents = repo.dirstate_parents()?; |
|
245 let mut revs = vec![]; |
|
246 let p1_rev = |
|
247 repo.changelog()? |
|
248 .rev_from_node(parents.p1.into()) |
|
249 .map_err(|_| { |
|
250 HgError::corrupted(format!( |
|
251 "dirstate points to non-existent parent node" |
|
252 )) |
|
253 })?; |
|
254 if p1_rev != NULL_REVISION { |
|
255 revs.push(p1_rev) |
|
256 } |
|
257 let p2_rev = |
|
258 repo.changelog()? |
|
259 .rev_from_node(parents.p2.into()) |
|
260 .map_err(|_| { |
|
261 HgError::corrupted(format!( |
|
262 "dirstate points to non-existent parent node" |
|
263 )) |
|
264 })?; |
|
265 if p2_rev != NULL_REVISION { |
|
266 revs.push(p2_rev) |
|
267 } |
|
268 let mut matchers = vec![]; |
|
269 |
|
270 for rev in revs.iter() { |
|
271 let config = patterns_for_rev(repo, *rev); |
|
272 if let Ok(Some(config)) = config { |
|
273 warnings.extend(config.warnings); |
|
274 let mut m: Box<dyn Matcher + Sync> = Box::new(AlwaysMatcher); |
|
275 if !config.includes.is_empty() { |
|
276 let (patterns, subwarnings) = parse_pattern_file_contents( |
|
277 &config.includes, |
|
278 Path::new(""), |
|
279 Some(b"relglob:".as_ref()), |
|
280 false, |
|
281 )?; |
|
282 warnings.extend(subwarnings.into_iter().map(From::from)); |
|
283 m = Box::new(IncludeMatcher::new(patterns)?); |
|
284 } |
|
285 if !config.excludes.is_empty() { |
|
286 let (patterns, subwarnings) = parse_pattern_file_contents( |
|
287 &config.excludes, |
|
288 Path::new(""), |
|
289 Some(b"relglob:".as_ref()), |
|
290 false, |
|
291 )?; |
|
292 warnings.extend(subwarnings.into_iter().map(From::from)); |
|
293 m = Box::new(DifferenceMatcher::new( |
|
294 m, |
|
295 Box::new(IncludeMatcher::new(patterns)?), |
|
296 )); |
|
297 } |
|
298 matchers.push(m); |
|
299 } |
|
300 } |
|
301 let result: Box<dyn Matcher + Sync> = match matchers.len() { |
|
302 0 => Box::new(AlwaysMatcher), |
|
303 1 => matchers.pop().expect("1 is equal to 0"), |
|
304 _ => Box::new(UnionMatcher::new(matchers)), |
|
305 }; |
|
306 |
|
307 let matcher = |
|
308 force_include_matcher(result, &read_temporary_includes(repo)?)?; |
|
309 Ok((matcher, warnings)) |
|
310 } |
|
311 |
|
312 /// Returns a matcher that returns true for any of the forced includes before |
|
313 /// testing against the actual matcher |
|
314 fn force_include_matcher( |
|
315 result: Box<dyn Matcher + Sync>, |
|
316 temp_includes: &[Vec<u8>], |
|
317 ) -> Result<Box<dyn Matcher + Sync>, PatternError> { |
|
318 if temp_includes.is_empty() { |
|
319 return Ok(result); |
|
320 } |
|
321 let forced_include_matcher = IncludeMatcher::new( |
|
322 temp_includes |
|
323 .into_iter() |
|
324 .map(|include| { |
|
325 IgnorePattern::new(PatternSyntax::Path, include, Path::new("")) |
|
326 }) |
|
327 .collect(), |
|
328 )?; |
|
329 Ok(Box::new(UnionMatcher::new(vec![ |
|
330 Box::new(forced_include_matcher), |
|
331 result, |
|
332 ]))) |
|
333 } |