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 |
|
254 | if repo.has_narrow() { | |
255 | return Err(CommandError::unsupported( |
|
255 | return Err(CommandError::unsupported( | |
256 |
"rhg status is not supported for |
|
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 |
|
|
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 |
|
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