49499
|
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 }
|