##// END OF EJS Templates
rhg: add sparse support
Raphaël Gomès -
r50380:ffd4b1f1 default
parent child Browse files
Show More
@@ -0,0 +1,333 b''
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 }
@@ -7,6 +7,7 b''
7 mod ancestors;
7 mod ancestors;
8 pub mod dagops;
8 pub mod dagops;
9 pub mod errors;
9 pub mod errors;
10 pub mod sparse;
10 pub use ancestors::{AncestorsIterator, MissingAncestors};
11 pub use ancestors::{AncestorsIterator, MissingAncestors};
11 pub mod dirstate;
12 pub mod dirstate;
12 pub mod dirstate_tree;
13 pub mod dirstate_tree;
@@ -40,6 +40,23 b" impl Vfs<'_> {"
40 std::fs::read(&path).when_reading_file(&path)
40 std::fs::read(&path).when_reading_file(&path)
41 }
41 }
42
42
43 /// Returns `Ok(None)` if the file does not exist.
44 pub fn try_read(
45 &self,
46 relative_path: impl AsRef<Path>,
47 ) -> Result<Option<Vec<u8>>, HgError> {
48 match self.read(relative_path) {
49 Err(e) => match &e {
50 HgError::IoError { error, .. } => match error.kind() {
51 ErrorKind::NotFound => return Ok(None),
52 _ => Err(e),
53 },
54 _ => Err(e),
55 },
56 Ok(v) => Ok(Some(v)),
57 }
58 }
59
43 fn mmap_open_gen(
60 fn mmap_open_gen(
44 &self,
61 &self,
45 relative_path: impl AsRef<Path>,
62 relative_path: impl AsRef<Path>,
@@ -18,8 +18,8 b' use hg::dirstate::TruncatedTimestamp;'
18 use hg::errors::{HgError, IoResultExt};
18 use hg::errors::{HgError, IoResultExt};
19 use hg::lock::LockError;
19 use hg::lock::LockError;
20 use hg::manifest::Manifest;
20 use hg::manifest::Manifest;
21 use hg::matchers::AlwaysMatcher;
22 use hg::repo::Repo;
21 use hg::repo::Repo;
22 use hg::sparse::{matcher, SparseWarning};
23 use hg::utils::files::get_bytes_from_os_string;
23 use hg::utils::files::get_bytes_from_os_string;
24 use hg::utils::files::get_bytes_from_path;
24 use hg::utils::files::get_bytes_from_path;
25 use hg::utils::files::get_path_from_bytes;
25 use hg::utils::files::get_path_from_bytes;
@@ -251,9 +251,9 b' pub fn run(invocation: &crate::CliInvoca'
251 };
251 };
252 }
252 }
253
253
254 if repo.has_sparse() || repo.has_narrow() {
254 if repo.has_narrow() {
255 return Err(CommandError::unsupported(
255 return Err(CommandError::unsupported(
256 "rhg status is not supported for sparse checkouts or narrow clones yet"
256 "rhg status is not supported for narrow clones yet",
257 ));
257 ));
258 }
258 }
259
259
@@ -366,9 +366,36 b' pub fn run(invocation: &crate::CliInvoca'
366 filesystem_time_at_status_start,
366 filesystem_time_at_status_start,
367 ))
367 ))
368 };
368 };
369 let (matcher, sparse_warnings) = matcher(repo)?;
370
371 for warning in sparse_warnings {
372 match &warning {
373 SparseWarning::RootWarning { context, line } => {
374 let msg = format_bytes!(
375 b"warning: {} profile cannot use paths \"
376 starting with /, ignoring {}\n",
377 context,
378 line
379 );
380 ui.write_stderr(&msg)?;
381 }
382 SparseWarning::ProfileNotFound { profile, rev } => {
383 let msg = format_bytes!(
384 b"warning: sparse profile '{}' not found \"
385 in rev {} - ignoring it\n",
386 profile,
387 rev
388 );
389 ui.write_stderr(&msg)?;
390 }
391 SparseWarning::Pattern(e) => {
392 ui.write_stderr(&print_pattern_file_warning(e, &repo))?;
393 }
394 }
395 }
369 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
396 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
370 dmap.with_status(
397 dmap.with_status(
371 &AlwaysMatcher,
398 matcher.as_ref(),
372 repo.working_directory_path().to_owned(),
399 repo.working_directory_path().to_owned(),
373 ignore_files(repo, config),
400 ignore_files(repo, config),
374 options,
401 options,
@@ -8,6 +8,7 b' use hg::errors::HgError;'
8 use hg::exit_codes;
8 use hg::exit_codes;
9 use hg::repo::RepoError;
9 use hg::repo::RepoError;
10 use hg::revlog::revlog::RevlogError;
10 use hg::revlog::revlog::RevlogError;
11 use hg::sparse::SparseConfigError;
11 use hg::utils::files::get_bytes_from_path;
12 use hg::utils::files::get_bytes_from_path;
12 use hg::{DirstateError, DirstateMapError, StatusError};
13 use hg::{DirstateError, DirstateMapError, StatusError};
13 use std::convert::From;
14 use std::convert::From;
@@ -52,6 +53,18 b' impl CommandError {'
52 }
53 }
53 }
54 }
54
55
56 pub fn abort_with_exit_code_bytes(
57 message: impl AsRef<[u8]>,
58 detailed_exit_code: exit_codes::ExitCode,
59 ) -> Self {
60 // TODO: use this everywhere it makes sense instead of the string
61 // version.
62 CommandError::Abort {
63 message: message.as_ref().into(),
64 detailed_exit_code,
65 }
66 }
67
55 pub fn unsupported(message: impl AsRef<str>) -> Self {
68 pub fn unsupported(message: impl AsRef<str>) -> Self {
56 CommandError::UnsupportedFeature {
69 CommandError::UnsupportedFeature {
57 message: utf8_to_local(message.as_ref()).into(),
70 message: utf8_to_local(message.as_ref()).into(),
@@ -212,3 +225,33 b' impl From<DirstateV2ParseError> for Comm'
212 HgError::from(error).into()
225 HgError::from(error).into()
213 }
226 }
214 }
227 }
228
229 impl From<SparseConfigError> for CommandError {
230 fn from(e: SparseConfigError) -> Self {
231 match e {
232 SparseConfigError::IncludesAfterExcludes { context } => {
233 Self::abort_with_exit_code_bytes(
234 format_bytes!(
235 b"{} config cannot have includes after excludes",
236 context
237 ),
238 exit_codes::CONFIG_PARSE_ERROR_ABORT,
239 )
240 }
241 SparseConfigError::EntryOutsideSection { context, line } => {
242 Self::abort_with_exit_code_bytes(
243 format_bytes!(
244 b"{} config entry outside of section: {}",
245 context,
246 &line,
247 ),
248 exit_codes::CONFIG_PARSE_ERROR_ABORT,
249 )
250 }
251 SparseConfigError::HgError(e) => Self::from(e),
252 SparseConfigError::PatternError(e) => {
253 Self::unsupported(format!("{}", e))
254 }
255 }
256 }
257 }
@@ -92,7 +92,7 b' support it in rhg for narrow clones yet.'
92 $ touch dir2/q
92 $ touch dir2/q
93 $ "$real_hg" status
93 $ "$real_hg" status
94 $ $NO_FALLBACK rhg --config rhg.status=true status
94 $ $NO_FALLBACK rhg --config rhg.status=true status
95 unsupported feature: rhg status is not supported for sparse checkouts or narrow clones yet
95 unsupported feature: rhg status is not supported for narrow clones yet
96 [252]
96 [252]
97
97
98 Adding "orphaned" index files:
98 Adding "orphaned" index files:
General Comments 0
You need to be logged in to leave comments. Login now