##// END OF EJS Templates
rhg: support "status FILE"...
Spencer Baugh -
r51759:c112cc9e default
parent child Browse files
Show More
@@ -1,833 +1,876 b''
1 // filepatterns.rs
1 // filepatterns.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Handling of Mercurial-specific patterns.
8 //! Handling of Mercurial-specific patterns.
9
9
10 use crate::{
10 use crate::{
11 utils::{
11 utils::{
12 files::{canonical_path, get_bytes_from_path, get_path_from_bytes},
12 files::{canonical_path, get_bytes_from_path, get_path_from_bytes},
13 hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError},
13 hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError},
14 SliceExt,
14 SliceExt,
15 },
15 },
16 FastHashMap, PatternError,
16 FastHashMap, PatternError,
17 };
17 };
18 use lazy_static::lazy_static;
18 use lazy_static::lazy_static;
19 use regex::bytes::{NoExpand, Regex};
19 use regex::bytes::{NoExpand, Regex};
20 use std::ops::Deref;
20 use std::ops::Deref;
21 use std::path::{Path, PathBuf};
21 use std::path::{Path, PathBuf};
22 use std::vec::Vec;
22 use std::vec::Vec;
23
23
24 lazy_static! {
24 lazy_static! {
25 static ref RE_ESCAPE: Vec<Vec<u8>> = {
25 static ref RE_ESCAPE: Vec<Vec<u8>> = {
26 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
26 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
27 let to_escape = b"()[]{}?*+-|^$\\.&~#\t\n\r\x0b\x0c";
27 let to_escape = b"()[]{}?*+-|^$\\.&~#\t\n\r\x0b\x0c";
28 for byte in to_escape {
28 for byte in to_escape {
29 v[*byte as usize].insert(0, b'\\');
29 v[*byte as usize].insert(0, b'\\');
30 }
30 }
31 v
31 v
32 };
32 };
33 }
33 }
34
34
35 /// These are matched in order
35 /// These are matched in order
36 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
36 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
37 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
37 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
38
38
39 #[derive(Debug, Clone, PartialEq, Eq)]
39 #[derive(Debug, Clone, PartialEq, Eq)]
40 pub enum PatternSyntax {
40 pub enum PatternSyntax {
41 /// A regular expression
41 /// A regular expression
42 Regexp,
42 Regexp,
43 /// Glob that matches at the front of the path
43 /// Glob that matches at the front of the path
44 RootGlob,
44 RootGlob,
45 /// Glob that matches at any suffix of the path (still anchored at
45 /// Glob that matches at any suffix of the path (still anchored at
46 /// slashes)
46 /// slashes)
47 Glob,
47 Glob,
48 /// a path relative to repository root, which is matched recursively
48 /// a path relative to repository root, which is matched recursively
49 Path,
49 Path,
50 /// a single exact path relative to repository root
50 /// a single exact path relative to repository root
51 FilePath,
51 FilePath,
52 /// A path relative to cwd
52 /// A path relative to cwd
53 RelPath,
53 RelPath,
54 /// an unrooted glob (*.rs matches Rust files in all dirs)
54 /// an unrooted glob (*.rs matches Rust files in all dirs)
55 RelGlob,
55 RelGlob,
56 /// A regexp that needn't match the start of a name
56 /// A regexp that needn't match the start of a name
57 RelRegexp,
57 RelRegexp,
58 /// A path relative to repository root, which is matched non-recursively
58 /// A path relative to repository root, which is matched non-recursively
59 /// (will not match subdirectories)
59 /// (will not match subdirectories)
60 RootFiles,
60 RootFiles,
61 /// A file of patterns to read and include
61 /// A file of patterns to read and include
62 Include,
62 Include,
63 /// A file of patterns to match against files under the same directory
63 /// A file of patterns to match against files under the same directory
64 SubInclude,
64 SubInclude,
65 /// SubInclude with the result of parsing the included file
65 /// SubInclude with the result of parsing the included file
66 ///
66 ///
67 /// Note: there is no ExpandedInclude because that expansion can be done
67 /// Note: there is no ExpandedInclude because that expansion can be done
68 /// in place by replacing the Include pattern by the included patterns.
68 /// in place by replacing the Include pattern by the included patterns.
69 /// SubInclude requires more handling.
69 /// SubInclude requires more handling.
70 ///
70 ///
71 /// Note: `Box` is used to minimize size impact on other enum variants
71 /// Note: `Box` is used to minimize size impact on other enum variants
72 ExpandedSubInclude(Box<SubInclude>),
72 ExpandedSubInclude(Box<SubInclude>),
73 }
73 }
74
74
75 /// Transforms a glob pattern into a regex
75 /// Transforms a glob pattern into a regex
76 fn glob_to_re(pat: &[u8]) -> Vec<u8> {
76 fn glob_to_re(pat: &[u8]) -> Vec<u8> {
77 let mut input = pat;
77 let mut input = pat;
78 let mut res: Vec<u8> = vec![];
78 let mut res: Vec<u8> = vec![];
79 let mut group_depth = 0;
79 let mut group_depth = 0;
80
80
81 while let Some((c, rest)) = input.split_first() {
81 while let Some((c, rest)) = input.split_first() {
82 input = rest;
82 input = rest;
83
83
84 match c {
84 match c {
85 b'*' => {
85 b'*' => {
86 for (source, repl) in GLOB_REPLACEMENTS {
86 for (source, repl) in GLOB_REPLACEMENTS {
87 if let Some(rest) = input.drop_prefix(source) {
87 if let Some(rest) = input.drop_prefix(source) {
88 input = rest;
88 input = rest;
89 res.extend(*repl);
89 res.extend(*repl);
90 break;
90 break;
91 }
91 }
92 }
92 }
93 }
93 }
94 b'?' => res.extend(b"."),
94 b'?' => res.extend(b"."),
95 b'[' => {
95 b'[' => {
96 match input.iter().skip(1).position(|b| *b == b']') {
96 match input.iter().skip(1).position(|b| *b == b']') {
97 None => res.extend(b"\\["),
97 None => res.extend(b"\\["),
98 Some(end) => {
98 Some(end) => {
99 // Account for the one we skipped
99 // Account for the one we skipped
100 let end = end + 1;
100 let end = end + 1;
101
101
102 res.extend(b"[");
102 res.extend(b"[");
103
103
104 for (i, b) in input[..end].iter().enumerate() {
104 for (i, b) in input[..end].iter().enumerate() {
105 if *b == b'!' && i == 0 {
105 if *b == b'!' && i == 0 {
106 res.extend(b"^")
106 res.extend(b"^")
107 } else if *b == b'^' && i == 0 {
107 } else if *b == b'^' && i == 0 {
108 res.extend(b"\\^")
108 res.extend(b"\\^")
109 } else if *b == b'\\' {
109 } else if *b == b'\\' {
110 res.extend(b"\\\\")
110 res.extend(b"\\\\")
111 } else {
111 } else {
112 res.push(*b)
112 res.push(*b)
113 }
113 }
114 }
114 }
115 res.extend(b"]");
115 res.extend(b"]");
116 input = &input[end + 1..];
116 input = &input[end + 1..];
117 }
117 }
118 }
118 }
119 }
119 }
120 b'{' => {
120 b'{' => {
121 group_depth += 1;
121 group_depth += 1;
122 res.extend(b"(?:")
122 res.extend(b"(?:")
123 }
123 }
124 b'}' if group_depth > 0 => {
124 b'}' if group_depth > 0 => {
125 group_depth -= 1;
125 group_depth -= 1;
126 res.extend(b")");
126 res.extend(b")");
127 }
127 }
128 b',' if group_depth > 0 => res.extend(b"|"),
128 b',' if group_depth > 0 => res.extend(b"|"),
129 b'\\' => {
129 b'\\' => {
130 let c = {
130 let c = {
131 if let Some((c, rest)) = input.split_first() {
131 if let Some((c, rest)) = input.split_first() {
132 input = rest;
132 input = rest;
133 c
133 c
134 } else {
134 } else {
135 c
135 c
136 }
136 }
137 };
137 };
138 res.extend(&RE_ESCAPE[*c as usize])
138 res.extend(&RE_ESCAPE[*c as usize])
139 }
139 }
140 _ => res.extend(&RE_ESCAPE[*c as usize]),
140 _ => res.extend(&RE_ESCAPE[*c as usize]),
141 }
141 }
142 }
142 }
143 res
143 res
144 }
144 }
145
145
146 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
146 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
147 pattern
147 pattern
148 .iter()
148 .iter()
149 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
149 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
150 .collect()
150 .collect()
151 }
151 }
152
152
153 pub fn parse_pattern_syntax(
153 pub fn parse_pattern_syntax(
154 kind: &[u8],
154 kind: &[u8],
155 ) -> Result<PatternSyntax, PatternError> {
155 ) -> Result<PatternSyntax, PatternError> {
156 match kind {
156 match kind {
157 b"re:" => Ok(PatternSyntax::Regexp),
157 b"re:" => Ok(PatternSyntax::Regexp),
158 b"path:" => Ok(PatternSyntax::Path),
158 b"path:" => Ok(PatternSyntax::Path),
159 b"filepath:" => Ok(PatternSyntax::FilePath),
159 b"filepath:" => Ok(PatternSyntax::FilePath),
160 b"relpath:" => Ok(PatternSyntax::RelPath),
160 b"relpath:" => Ok(PatternSyntax::RelPath),
161 b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
161 b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
162 b"relglob:" => Ok(PatternSyntax::RelGlob),
162 b"relglob:" => Ok(PatternSyntax::RelGlob),
163 b"relre:" => Ok(PatternSyntax::RelRegexp),
163 b"relre:" => Ok(PatternSyntax::RelRegexp),
164 b"glob:" => Ok(PatternSyntax::Glob),
164 b"glob:" => Ok(PatternSyntax::Glob),
165 b"rootglob:" => Ok(PatternSyntax::RootGlob),
165 b"rootglob:" => Ok(PatternSyntax::RootGlob),
166 b"include:" => Ok(PatternSyntax::Include),
166 b"include:" => Ok(PatternSyntax::Include),
167 b"subinclude:" => Ok(PatternSyntax::SubInclude),
167 b"subinclude:" => Ok(PatternSyntax::SubInclude),
168 _ => Err(PatternError::UnsupportedSyntax(
168 _ => Err(PatternError::UnsupportedSyntax(
169 String::from_utf8_lossy(kind).to_string(),
169 String::from_utf8_lossy(kind).to_string(),
170 )),
170 )),
171 }
171 }
172 }
172 }
173
173
174 lazy_static! {
174 lazy_static! {
175 static ref FLAG_RE: Regex = Regex::new(r"^\(\?[aiLmsux]+\)").unwrap();
175 static ref FLAG_RE: Regex = Regex::new(r"^\(\?[aiLmsux]+\)").unwrap();
176 }
176 }
177
177
178 /// Builds the regex that corresponds to the given pattern.
178 /// Builds the regex that corresponds to the given pattern.
179 /// If within a `syntax: regexp` context, returns the pattern,
179 /// If within a `syntax: regexp` context, returns the pattern,
180 /// otherwise, returns the corresponding regex.
180 /// otherwise, returns the corresponding regex.
181 fn _build_single_regex(entry: &IgnorePattern, glob_suffix: &[u8]) -> Vec<u8> {
181 fn _build_single_regex(entry: &IgnorePattern, glob_suffix: &[u8]) -> Vec<u8> {
182 let IgnorePattern {
182 let IgnorePattern {
183 syntax, pattern, ..
183 syntax, pattern, ..
184 } = entry;
184 } = entry;
185 if pattern.is_empty() {
185 if pattern.is_empty() {
186 return vec![];
186 return vec![];
187 }
187 }
188 match syntax {
188 match syntax {
189 PatternSyntax::Regexp => pattern.to_owned(),
189 PatternSyntax::Regexp => pattern.to_owned(),
190 PatternSyntax::RelRegexp => {
190 PatternSyntax::RelRegexp => {
191 // The `regex` crate accepts `**` while `re2` and Python's `re`
191 // The `regex` crate accepts `**` while `re2` and Python's `re`
192 // do not. Checking for `*` correctly triggers the same error all
192 // do not. Checking for `*` correctly triggers the same error all
193 // engines.
193 // engines.
194 if pattern[0] == b'^'
194 if pattern[0] == b'^'
195 || pattern[0] == b'*'
195 || pattern[0] == b'*'
196 || pattern.starts_with(b".*")
196 || pattern.starts_with(b".*")
197 {
197 {
198 return pattern.to_owned();
198 return pattern.to_owned();
199 }
199 }
200 match FLAG_RE.find(pattern) {
200 match FLAG_RE.find(pattern) {
201 Some(mat) => {
201 Some(mat) => {
202 let s = mat.start();
202 let s = mat.start();
203 let e = mat.end();
203 let e = mat.end();
204 [
204 [
205 &b"(?"[..],
205 &b"(?"[..],
206 &pattern[s + 2..e - 1],
206 &pattern[s + 2..e - 1],
207 &b":"[..],
207 &b":"[..],
208 if pattern[e] == b'^'
208 if pattern[e] == b'^'
209 || pattern[e] == b'*'
209 || pattern[e] == b'*'
210 || pattern[e..].starts_with(b".*")
210 || pattern[e..].starts_with(b".*")
211 {
211 {
212 &b""[..]
212 &b""[..]
213 } else {
213 } else {
214 &b".*"[..]
214 &b".*"[..]
215 },
215 },
216 &pattern[e..],
216 &pattern[e..],
217 &b")"[..],
217 &b")"[..],
218 ]
218 ]
219 .concat()
219 .concat()
220 }
220 }
221 None => [&b".*"[..], pattern].concat(),
221 None => [&b".*"[..], pattern].concat(),
222 }
222 }
223 }
223 }
224 PatternSyntax::Path | PatternSyntax::RelPath => {
224 PatternSyntax::Path | PatternSyntax::RelPath => {
225 if pattern == b"." {
225 if pattern == b"." {
226 return vec![];
226 return vec![];
227 }
227 }
228 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
228 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
229 }
229 }
230 PatternSyntax::RootFiles => {
230 PatternSyntax::RootFiles => {
231 let mut res = if pattern == b"." {
231 let mut res = if pattern == b"." {
232 vec![]
232 vec![]
233 } else {
233 } else {
234 // Pattern is a directory name.
234 // Pattern is a directory name.
235 [escape_pattern(pattern).as_slice(), b"/"].concat()
235 [escape_pattern(pattern).as_slice(), b"/"].concat()
236 };
236 };
237
237
238 // Anything after the pattern must be a non-directory.
238 // Anything after the pattern must be a non-directory.
239 res.extend(b"[^/]+$");
239 res.extend(b"[^/]+$");
240 res
240 res
241 }
241 }
242 PatternSyntax::RelGlob => {
242 PatternSyntax::RelGlob => {
243 let glob_re = glob_to_re(pattern);
243 let glob_re = glob_to_re(pattern);
244 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
244 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
245 [b".*", rest, glob_suffix].concat()
245 [b".*", rest, glob_suffix].concat()
246 } else {
246 } else {
247 [b"(?:.*/)?", glob_re.as_slice(), glob_suffix].concat()
247 [b"(?:.*/)?", glob_re.as_slice(), glob_suffix].concat()
248 }
248 }
249 }
249 }
250 PatternSyntax::Glob | PatternSyntax::RootGlob => {
250 PatternSyntax::Glob | PatternSyntax::RootGlob => {
251 [glob_to_re(pattern).as_slice(), glob_suffix].concat()
251 [glob_to_re(pattern).as_slice(), glob_suffix].concat()
252 }
252 }
253 PatternSyntax::Include
253 PatternSyntax::Include
254 | PatternSyntax::SubInclude
254 | PatternSyntax::SubInclude
255 | PatternSyntax::ExpandedSubInclude(_)
255 | PatternSyntax::ExpandedSubInclude(_)
256 | PatternSyntax::FilePath => unreachable!(),
256 | PatternSyntax::FilePath => unreachable!(),
257 }
257 }
258 }
258 }
259
259
260 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
260 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
261 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
261 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
262
262
263 /// TODO support other platforms
263 /// TODO support other platforms
264 #[cfg(unix)]
264 #[cfg(unix)]
265 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
265 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
266 if bytes.is_empty() {
266 if bytes.is_empty() {
267 return b".".to_vec();
267 return b".".to_vec();
268 }
268 }
269 let sep = b'/';
269 let sep = b'/';
270
270
271 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
271 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
272 if initial_slashes > 2 {
272 if initial_slashes > 2 {
273 // POSIX allows one or two initial slashes, but treats three or more
273 // POSIX allows one or two initial slashes, but treats three or more
274 // as single slash.
274 // as single slash.
275 initial_slashes = 1;
275 initial_slashes = 1;
276 }
276 }
277 let components = bytes
277 let components = bytes
278 .split(|b| *b == sep)
278 .split(|b| *b == sep)
279 .filter(|c| !(c.is_empty() || c == b"."))
279 .filter(|c| !(c.is_empty() || c == b"."))
280 .fold(vec![], |mut acc, component| {
280 .fold(vec![], |mut acc, component| {
281 if component != b".."
281 if component != b".."
282 || (initial_slashes == 0 && acc.is_empty())
282 || (initial_slashes == 0 && acc.is_empty())
283 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
283 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
284 {
284 {
285 acc.push(component)
285 acc.push(component)
286 } else if !acc.is_empty() {
286 } else if !acc.is_empty() {
287 acc.pop();
287 acc.pop();
288 }
288 }
289 acc
289 acc
290 });
290 });
291 let mut new_bytes = components.join(&sep);
291 let mut new_bytes = components.join(&sep);
292
292
293 if initial_slashes > 0 {
293 if initial_slashes > 0 {
294 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
294 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
295 buf.extend(new_bytes);
295 buf.extend(new_bytes);
296 new_bytes = buf;
296 new_bytes = buf;
297 }
297 }
298 if new_bytes.is_empty() {
298 if new_bytes.is_empty() {
299 b".".to_vec()
299 b".".to_vec()
300 } else {
300 } else {
301 new_bytes
301 new_bytes
302 }
302 }
303 }
303 }
304
304
305 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
305 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
306 /// that don't need to be transformed into a regex.
306 /// that don't need to be transformed into a regex.
307 pub fn build_single_regex(
307 pub fn build_single_regex(
308 entry: &IgnorePattern,
308 entry: &IgnorePattern,
309 glob_suffix: &[u8],
309 glob_suffix: &[u8],
310 ) -> Result<Option<Vec<u8>>, PatternError> {
310 ) -> Result<Option<Vec<u8>>, PatternError> {
311 let IgnorePattern {
311 let IgnorePattern {
312 pattern, syntax, ..
312 pattern, syntax, ..
313 } = entry;
313 } = entry;
314 let pattern = match syntax {
314 let pattern = match syntax {
315 PatternSyntax::RootGlob
315 PatternSyntax::RootGlob
316 | PatternSyntax::Path
316 | PatternSyntax::Path
317 | PatternSyntax::RelGlob
317 | PatternSyntax::RelGlob
318 | PatternSyntax::RelPath
318 | PatternSyntax::RelPath
319 | PatternSyntax::RootFiles => normalize_path_bytes(pattern),
319 | PatternSyntax::RootFiles => normalize_path_bytes(pattern),
320 PatternSyntax::Include | PatternSyntax::SubInclude => {
320 PatternSyntax::Include | PatternSyntax::SubInclude => {
321 return Err(PatternError::NonRegexPattern(entry.clone()))
321 return Err(PatternError::NonRegexPattern(entry.clone()))
322 }
322 }
323 _ => pattern.to_owned(),
323 _ => pattern.to_owned(),
324 };
324 };
325 let is_simple_rootglob = *syntax == PatternSyntax::RootGlob
325 let is_simple_rootglob = *syntax == PatternSyntax::RootGlob
326 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b));
326 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b));
327 if is_simple_rootglob || syntax == &PatternSyntax::FilePath {
327 if is_simple_rootglob || syntax == &PatternSyntax::FilePath {
328 Ok(None)
328 Ok(None)
329 } else {
329 } else {
330 let mut entry = entry.clone();
330 let mut entry = entry.clone();
331 entry.pattern = pattern;
331 entry.pattern = pattern;
332 Ok(Some(_build_single_regex(&entry, glob_suffix)))
332 Ok(Some(_build_single_regex(&entry, glob_suffix)))
333 }
333 }
334 }
334 }
335
335
336 lazy_static! {
336 lazy_static! {
337 static ref SYNTAXES: FastHashMap<&'static [u8], PatternSyntax> = {
337 static ref SYNTAXES: FastHashMap<&'static [u8], PatternSyntax> = {
338 let mut m = FastHashMap::default();
338 let mut m = FastHashMap::default();
339
339
340 m.insert(b"re:".as_ref(), PatternSyntax::Regexp);
340 m.insert(b"re:".as_ref(), PatternSyntax::Regexp);
341 m.insert(b"regexp:".as_ref(), PatternSyntax::Regexp);
341 m.insert(b"regexp:".as_ref(), PatternSyntax::Regexp);
342 m.insert(b"path:".as_ref(), PatternSyntax::Path);
342 m.insert(b"path:".as_ref(), PatternSyntax::Path);
343 m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
343 m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
344 m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
344 m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
345 m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFiles);
345 m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFiles);
346 m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
346 m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
347 m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
347 m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
348 m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
348 m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
349 m.insert(b"rootglob:".as_ref(), PatternSyntax::RootGlob);
349 m.insert(b"rootglob:".as_ref(), PatternSyntax::RootGlob);
350 m.insert(b"include:".as_ref(), PatternSyntax::Include);
350 m.insert(b"include:".as_ref(), PatternSyntax::Include);
351 m.insert(b"subinclude:".as_ref(), PatternSyntax::SubInclude);
351 m.insert(b"subinclude:".as_ref(), PatternSyntax::SubInclude);
352
352
353 m
353 m
354 };
354 };
355 }
355 }
356
356
357 #[derive(Debug)]
357 #[derive(Debug)]
358 pub enum PatternFileWarning {
358 pub enum PatternFileWarning {
359 /// (file path, syntax bytes)
359 /// (file path, syntax bytes)
360 InvalidSyntax(PathBuf, Vec<u8>),
360 InvalidSyntax(PathBuf, Vec<u8>),
361 /// File path
361 /// File path
362 NoSuchFile(PathBuf),
362 NoSuchFile(PathBuf),
363 }
363 }
364
364
365 pub fn parse_one_pattern(
365 pub fn parse_one_pattern(
366 pattern: &[u8],
366 pattern: &[u8],
367 source: &Path,
367 source: &Path,
368 default: PatternSyntax,
368 default: PatternSyntax,
369 normalize: bool,
369 ) -> IgnorePattern {
370 ) -> IgnorePattern {
370 let mut pattern_bytes: &[u8] = pattern;
371 let mut pattern_bytes: &[u8] = pattern;
371 let mut syntax = default;
372 let mut syntax = default;
372
373
373 for (s, val) in SYNTAXES.iter() {
374 for (s, val) in SYNTAXES.iter() {
374 if let Some(rest) = pattern_bytes.drop_prefix(s) {
375 if let Some(rest) = pattern_bytes.drop_prefix(s) {
375 syntax = val.clone();
376 syntax = val.clone();
376 pattern_bytes = rest;
377 pattern_bytes = rest;
377 break;
378 break;
378 }
379 }
379 }
380 }
380
381
381 let pattern = pattern_bytes.to_vec();
382 let pattern = match syntax {
383 PatternSyntax::RootGlob
384 | PatternSyntax::Path
385 | PatternSyntax::Glob
386 | PatternSyntax::RelGlob
387 | PatternSyntax::RelPath
388 | PatternSyntax::RootFiles
389 if normalize =>
390 {
391 normalize_path_bytes(pattern_bytes)
392 }
393 _ => pattern_bytes.to_vec(),
394 };
382
395
383 IgnorePattern {
396 IgnorePattern {
384 syntax,
397 syntax,
385 pattern,
398 pattern,
386 source: source.to_owned(),
399 source: source.to_owned(),
387 }
400 }
388 }
401 }
389
402
390 pub fn parse_pattern_file_contents(
403 pub fn parse_pattern_file_contents(
391 lines: &[u8],
404 lines: &[u8],
392 file_path: &Path,
405 file_path: &Path,
393 default_syntax_override: Option<PatternSyntax>,
406 default_syntax_override: Option<PatternSyntax>,
394 warn: bool,
407 warn: bool,
395 relativize: bool,
408 relativize: bool,
396 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
409 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
397 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
410 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
398
411
399 #[allow(clippy::trivial_regex)]
412 #[allow(clippy::trivial_regex)]
400 let comment_escape_regex = Regex::new(r"\\#").unwrap();
413 let comment_escape_regex = Regex::new(r"\\#").unwrap();
401 let mut inputs: Vec<IgnorePattern> = vec![];
414 let mut inputs: Vec<IgnorePattern> = vec![];
402 let mut warnings: Vec<PatternFileWarning> = vec![];
415 let mut warnings: Vec<PatternFileWarning> = vec![];
403
416
404 let mut current_syntax =
417 let mut current_syntax =
405 default_syntax_override.unwrap_or(PatternSyntax::RelRegexp);
418 default_syntax_override.unwrap_or(PatternSyntax::RelRegexp);
406
419
407 for mut line in lines.split(|c| *c == b'\n') {
420 for mut line in lines.split(|c| *c == b'\n') {
408 let line_buf;
421 let line_buf;
409 if line.contains(&b'#') {
422 if line.contains(&b'#') {
410 if let Some(cap) = comment_regex.captures(line) {
423 if let Some(cap) = comment_regex.captures(line) {
411 line = &line[..cap.get(1).unwrap().end()]
424 line = &line[..cap.get(1).unwrap().end()]
412 }
425 }
413 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
426 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
414 line = &line_buf;
427 line = &line_buf;
415 }
428 }
416
429
417 let line = line.trim_end();
430 let line = line.trim_end();
418
431
419 if line.is_empty() {
432 if line.is_empty() {
420 continue;
433 continue;
421 }
434 }
422
435
423 if let Some(syntax) = line.drop_prefix(b"syntax:") {
436 if let Some(syntax) = line.drop_prefix(b"syntax:") {
424 let syntax = syntax.trim();
437 let syntax = syntax.trim();
425
438
426 if let Some(parsed) =
439 if let Some(parsed) =
427 SYNTAXES.get([syntax, &b":"[..]].concat().as_slice())
440 SYNTAXES.get([syntax, &b":"[..]].concat().as_slice())
428 {
441 {
429 current_syntax = parsed.clone();
442 current_syntax = parsed.clone();
430 } else if warn {
443 } else if warn {
431 warnings.push(PatternFileWarning::InvalidSyntax(
444 warnings.push(PatternFileWarning::InvalidSyntax(
432 file_path.to_owned(),
445 file_path.to_owned(),
433 syntax.to_owned(),
446 syntax.to_owned(),
434 ));
447 ));
435 }
448 }
436 } else {
449 } else {
437 let pattern = parse_one_pattern(
450 let pattern = parse_one_pattern(
438 line,
451 line,
439 file_path,
452 file_path,
440 current_syntax.clone(),
453 current_syntax.clone(),
454 false,
441 );
455 );
442 inputs.push(if relativize {
456 inputs.push(if relativize {
443 pattern.to_relative()
457 pattern.to_relative()
444 } else {
458 } else {
445 pattern
459 pattern
446 })
460 })
447 }
461 }
448 }
462 }
449 Ok((inputs, warnings))
463 Ok((inputs, warnings))
450 }
464 }
451
465
466 pub fn parse_pattern_args(
467 patterns: Vec<Vec<u8>>,
468 cwd: &Path,
469 root: &Path,
470 ) -> Result<Vec<IgnorePattern>, HgPathError> {
471 let mut ignore_patterns: Vec<IgnorePattern> = Vec::new();
472 for pattern in patterns {
473 let pattern = parse_one_pattern(
474 &pattern,
475 Path::new("<args>"),
476 PatternSyntax::RelPath,
477 true,
478 );
479 match pattern.syntax {
480 PatternSyntax::RelGlob | PatternSyntax::RelPath => {
481 let name = get_path_from_bytes(&pattern.pattern);
482 let canon = canonical_path(root, cwd, name)?;
483 ignore_patterns.push(IgnorePattern {
484 syntax: pattern.syntax,
485 pattern: get_bytes_from_path(canon),
486 source: pattern.source,
487 })
488 }
489 _ => ignore_patterns.push(pattern.to_owned()),
490 };
491 }
492 Ok(ignore_patterns)
493 }
494
452 pub fn read_pattern_file(
495 pub fn read_pattern_file(
453 file_path: &Path,
496 file_path: &Path,
454 warn: bool,
497 warn: bool,
455 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
498 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
456 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
499 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
457 match std::fs::read(file_path) {
500 match std::fs::read(file_path) {
458 Ok(contents) => {
501 Ok(contents) => {
459 inspect_pattern_bytes(file_path, &contents);
502 inspect_pattern_bytes(file_path, &contents);
460 parse_pattern_file_contents(&contents, file_path, None, warn, true)
503 parse_pattern_file_contents(&contents, file_path, None, warn, true)
461 }
504 }
462 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
505 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
463 vec![],
506 vec![],
464 vec![PatternFileWarning::NoSuchFile(file_path.to_owned())],
507 vec![PatternFileWarning::NoSuchFile(file_path.to_owned())],
465 )),
508 )),
466 Err(e) => Err(e.into()),
509 Err(e) => Err(e.into()),
467 }
510 }
468 }
511 }
469
512
470 /// Represents an entry in an "ignore" file.
513 /// Represents an entry in an "ignore" file.
471 #[derive(Debug, Eq, PartialEq, Clone)]
514 #[derive(Debug, Eq, PartialEq, Clone)]
472 pub struct IgnorePattern {
515 pub struct IgnorePattern {
473 pub syntax: PatternSyntax,
516 pub syntax: PatternSyntax,
474 pub pattern: Vec<u8>,
517 pub pattern: Vec<u8>,
475 pub source: PathBuf,
518 pub source: PathBuf,
476 }
519 }
477
520
478 impl IgnorePattern {
521 impl IgnorePattern {
479 pub fn new(syntax: PatternSyntax, pattern: &[u8], source: &Path) -> Self {
522 pub fn new(syntax: PatternSyntax, pattern: &[u8], source: &Path) -> Self {
480 Self {
523 Self {
481 syntax,
524 syntax,
482 pattern: pattern.to_owned(),
525 pattern: pattern.to_owned(),
483 source: source.to_owned(),
526 source: source.to_owned(),
484 }
527 }
485 }
528 }
486
529
487 pub fn to_relative(self) -> Self {
530 pub fn to_relative(self) -> Self {
488 let Self {
531 let Self {
489 syntax,
532 syntax,
490 pattern,
533 pattern,
491 source,
534 source,
492 } = self;
535 } = self;
493 Self {
536 Self {
494 syntax: match syntax {
537 syntax: match syntax {
495 PatternSyntax::Regexp => PatternSyntax::RelRegexp,
538 PatternSyntax::Regexp => PatternSyntax::RelRegexp,
496 PatternSyntax::Glob => PatternSyntax::RelGlob,
539 PatternSyntax::Glob => PatternSyntax::RelGlob,
497 x => x,
540 x => x,
498 },
541 },
499 pattern,
542 pattern,
500 source,
543 source,
501 }
544 }
502 }
545 }
503 }
546 }
504
547
505 pub type PatternResult<T> = Result<T, PatternError>;
548 pub type PatternResult<T> = Result<T, PatternError>;
506
549
507 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
550 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
508 /// and `subinclude:` patterns.
551 /// and `subinclude:` patterns.
509 ///
552 ///
510 /// The former are expanded in place, while `PatternSyntax::ExpandedSubInclude`
553 /// The former are expanded in place, while `PatternSyntax::ExpandedSubInclude`
511 /// is used for the latter to form a tree of patterns.
554 /// is used for the latter to form a tree of patterns.
512 pub fn get_patterns_from_file(
555 pub fn get_patterns_from_file(
513 pattern_file: &Path,
556 pattern_file: &Path,
514 root_dir: &Path,
557 root_dir: &Path,
515 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
558 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
516 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
559 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
517 let (patterns, mut warnings) =
560 let (patterns, mut warnings) =
518 read_pattern_file(pattern_file, true, inspect_pattern_bytes)?;
561 read_pattern_file(pattern_file, true, inspect_pattern_bytes)?;
519 let patterns = patterns
562 let patterns = patterns
520 .into_iter()
563 .into_iter()
521 .flat_map(|entry| -> PatternResult<_> {
564 .flat_map(|entry| -> PatternResult<_> {
522 Ok(match &entry.syntax {
565 Ok(match &entry.syntax {
523 PatternSyntax::Include => {
566 PatternSyntax::Include => {
524 let inner_include =
567 let inner_include =
525 root_dir.join(get_path_from_bytes(&entry.pattern));
568 root_dir.join(get_path_from_bytes(&entry.pattern));
526 let (inner_pats, inner_warnings) = get_patterns_from_file(
569 let (inner_pats, inner_warnings) = get_patterns_from_file(
527 &inner_include,
570 &inner_include,
528 root_dir,
571 root_dir,
529 inspect_pattern_bytes,
572 inspect_pattern_bytes,
530 )?;
573 )?;
531 warnings.extend(inner_warnings);
574 warnings.extend(inner_warnings);
532 inner_pats
575 inner_pats
533 }
576 }
534 PatternSyntax::SubInclude => {
577 PatternSyntax::SubInclude => {
535 let mut sub_include = SubInclude::new(
578 let mut sub_include = SubInclude::new(
536 root_dir,
579 root_dir,
537 &entry.pattern,
580 &entry.pattern,
538 &entry.source,
581 &entry.source,
539 )?;
582 )?;
540 let (inner_patterns, inner_warnings) =
583 let (inner_patterns, inner_warnings) =
541 get_patterns_from_file(
584 get_patterns_from_file(
542 &sub_include.path,
585 &sub_include.path,
543 &sub_include.root,
586 &sub_include.root,
544 inspect_pattern_bytes,
587 inspect_pattern_bytes,
545 )?;
588 )?;
546 sub_include.included_patterns = inner_patterns;
589 sub_include.included_patterns = inner_patterns;
547 warnings.extend(inner_warnings);
590 warnings.extend(inner_warnings);
548 vec![IgnorePattern {
591 vec![IgnorePattern {
549 syntax: PatternSyntax::ExpandedSubInclude(Box::new(
592 syntax: PatternSyntax::ExpandedSubInclude(Box::new(
550 sub_include,
593 sub_include,
551 )),
594 )),
552 ..entry
595 ..entry
553 }]
596 }]
554 }
597 }
555 _ => vec![entry],
598 _ => vec![entry],
556 })
599 })
557 })
600 })
558 .flatten()
601 .flatten()
559 .collect();
602 .collect();
560
603
561 Ok((patterns, warnings))
604 Ok((patterns, warnings))
562 }
605 }
563
606
564 /// Holds all the information needed to handle a `subinclude:` pattern.
607 /// Holds all the information needed to handle a `subinclude:` pattern.
565 #[derive(Debug, PartialEq, Eq, Clone)]
608 #[derive(Debug, PartialEq, Eq, Clone)]
566 pub struct SubInclude {
609 pub struct SubInclude {
567 /// Will be used for repository (hg) paths that start with this prefix.
610 /// Will be used for repository (hg) paths that start with this prefix.
568 /// It is relative to the current working directory, so comparing against
611 /// It is relative to the current working directory, so comparing against
569 /// repository paths is painless.
612 /// repository paths is painless.
570 pub prefix: HgPathBuf,
613 pub prefix: HgPathBuf,
571 /// The file itself, containing the patterns
614 /// The file itself, containing the patterns
572 pub path: PathBuf,
615 pub path: PathBuf,
573 /// Folder in the filesystem where this it applies
616 /// Folder in the filesystem where this it applies
574 pub root: PathBuf,
617 pub root: PathBuf,
575
618
576 pub included_patterns: Vec<IgnorePattern>,
619 pub included_patterns: Vec<IgnorePattern>,
577 }
620 }
578
621
579 impl SubInclude {
622 impl SubInclude {
580 pub fn new(
623 pub fn new(
581 root_dir: &Path,
624 root_dir: &Path,
582 pattern: &[u8],
625 pattern: &[u8],
583 source: &Path,
626 source: &Path,
584 ) -> Result<SubInclude, HgPathError> {
627 ) -> Result<SubInclude, HgPathError> {
585 let normalized_source =
628 let normalized_source =
586 normalize_path_bytes(&get_bytes_from_path(source));
629 normalize_path_bytes(&get_bytes_from_path(source));
587
630
588 let source_root = get_path_from_bytes(&normalized_source);
631 let source_root = get_path_from_bytes(&normalized_source);
589 let source_root =
632 let source_root =
590 source_root.parent().unwrap_or_else(|| source_root.deref());
633 source_root.parent().unwrap_or_else(|| source_root.deref());
591
634
592 let path = source_root.join(get_path_from_bytes(pattern));
635 let path = source_root.join(get_path_from_bytes(pattern));
593 let new_root = path.parent().unwrap_or_else(|| path.deref());
636 let new_root = path.parent().unwrap_or_else(|| path.deref());
594
637
595 let prefix = canonical_path(root_dir, root_dir, new_root)?;
638 let prefix = canonical_path(root_dir, root_dir, new_root)?;
596
639
597 Ok(Self {
640 Ok(Self {
598 prefix: path_to_hg_path_buf(prefix).map(|mut p| {
641 prefix: path_to_hg_path_buf(prefix).map(|mut p| {
599 if !p.is_empty() {
642 if !p.is_empty() {
600 p.push_byte(b'/');
643 p.push_byte(b'/');
601 }
644 }
602 p
645 p
603 })?,
646 })?,
604 path: path.to_owned(),
647 path: path.to_owned(),
605 root: new_root.to_owned(),
648 root: new_root.to_owned(),
606 included_patterns: Vec::new(),
649 included_patterns: Vec::new(),
607 })
650 })
608 }
651 }
609 }
652 }
610
653
611 /// Separate and pre-process subincludes from other patterns for the "ignore"
654 /// Separate and pre-process subincludes from other patterns for the "ignore"
612 /// phase.
655 /// phase.
613 pub fn filter_subincludes(
656 pub fn filter_subincludes(
614 ignore_patterns: Vec<IgnorePattern>,
657 ignore_patterns: Vec<IgnorePattern>,
615 ) -> Result<(Vec<SubInclude>, Vec<IgnorePattern>), HgPathError> {
658 ) -> Result<(Vec<SubInclude>, Vec<IgnorePattern>), HgPathError> {
616 let mut subincludes = vec![];
659 let mut subincludes = vec![];
617 let mut others = vec![];
660 let mut others = vec![];
618
661
619 for pattern in ignore_patterns {
662 for pattern in ignore_patterns {
620 if let PatternSyntax::ExpandedSubInclude(sub_include) = pattern.syntax
663 if let PatternSyntax::ExpandedSubInclude(sub_include) = pattern.syntax
621 {
664 {
622 subincludes.push(*sub_include);
665 subincludes.push(*sub_include);
623 } else {
666 } else {
624 others.push(pattern)
667 others.push(pattern)
625 }
668 }
626 }
669 }
627 Ok((subincludes, others))
670 Ok((subincludes, others))
628 }
671 }
629
672
630 #[cfg(test)]
673 #[cfg(test)]
631 mod tests {
674 mod tests {
632 use super::*;
675 use super::*;
633 use pretty_assertions::assert_eq;
676 use pretty_assertions::assert_eq;
634
677
635 #[test]
678 #[test]
636 fn escape_pattern_test() {
679 fn escape_pattern_test() {
637 let untouched =
680 let untouched =
638 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
681 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
639 assert_eq!(escape_pattern(untouched), untouched.to_vec());
682 assert_eq!(escape_pattern(untouched), untouched.to_vec());
640 // All escape codes
683 // All escape codes
641 assert_eq!(
684 assert_eq!(
642 escape_pattern(br#"()[]{}?*+-|^$\\.&~#\t\n\r\v\f"#),
685 escape_pattern(br#"()[]{}?*+-|^$\\.&~#\t\n\r\v\f"#),
643 br#"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\\t\\n\\r\\v\\f"#
686 br#"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\\t\\n\\r\\v\\f"#
644 .to_vec()
687 .to_vec()
645 );
688 );
646 }
689 }
647
690
648 #[test]
691 #[test]
649 fn glob_test() {
692 fn glob_test() {
650 assert_eq!(glob_to_re(br#"?"#), br#"."#);
693 assert_eq!(glob_to_re(br#"?"#), br#"."#);
651 assert_eq!(glob_to_re(br#"*"#), br#"[^/]*"#);
694 assert_eq!(glob_to_re(br#"*"#), br#"[^/]*"#);
652 assert_eq!(glob_to_re(br#"**"#), br#".*"#);
695 assert_eq!(glob_to_re(br#"**"#), br#".*"#);
653 assert_eq!(glob_to_re(br#"**/a"#), br#"(?:.*/)?a"#);
696 assert_eq!(glob_to_re(br#"**/a"#), br#"(?:.*/)?a"#);
654 assert_eq!(glob_to_re(br#"a/**/b"#), br#"a/(?:.*/)?b"#);
697 assert_eq!(glob_to_re(br#"a/**/b"#), br#"a/(?:.*/)?b"#);
655 assert_eq!(glob_to_re(br#"[a*?!^][^b][!c]"#), br#"[a*?!^][\^b][^c]"#);
698 assert_eq!(glob_to_re(br#"[a*?!^][^b][!c]"#), br#"[a*?!^][\^b][^c]"#);
656 assert_eq!(glob_to_re(br#"{a,b}"#), br#"(?:a|b)"#);
699 assert_eq!(glob_to_re(br#"{a,b}"#), br#"(?:a|b)"#);
657 assert_eq!(glob_to_re(br#".\*\?"#), br#"\.\*\?"#);
700 assert_eq!(glob_to_re(br#".\*\?"#), br#"\.\*\?"#);
658 }
701 }
659
702
660 #[test]
703 #[test]
661 fn test_parse_pattern_file_contents() {
704 fn test_parse_pattern_file_contents() {
662 let lines = b"syntax: glob\n*.elc";
705 let lines = b"syntax: glob\n*.elc";
663
706
664 assert_eq!(
707 assert_eq!(
665 parse_pattern_file_contents(
708 parse_pattern_file_contents(
666 lines,
709 lines,
667 Path::new("file_path"),
710 Path::new("file_path"),
668 None,
711 None,
669 false,
712 false,
670 true,
713 true,
671 )
714 )
672 .unwrap()
715 .unwrap()
673 .0,
716 .0,
674 vec![IgnorePattern::new(
717 vec![IgnorePattern::new(
675 PatternSyntax::RelGlob,
718 PatternSyntax::RelGlob,
676 b"*.elc",
719 b"*.elc",
677 Path::new("file_path")
720 Path::new("file_path")
678 )],
721 )],
679 );
722 );
680
723
681 let lines = b"syntax: include\nsyntax: glob";
724 let lines = b"syntax: include\nsyntax: glob";
682
725
683 assert_eq!(
726 assert_eq!(
684 parse_pattern_file_contents(
727 parse_pattern_file_contents(
685 lines,
728 lines,
686 Path::new("file_path"),
729 Path::new("file_path"),
687 None,
730 None,
688 false,
731 false,
689 true,
732 true,
690 )
733 )
691 .unwrap()
734 .unwrap()
692 .0,
735 .0,
693 vec![]
736 vec![]
694 );
737 );
695 let lines = b"glob:**.o";
738 let lines = b"glob:**.o";
696 assert_eq!(
739 assert_eq!(
697 parse_pattern_file_contents(
740 parse_pattern_file_contents(
698 lines,
741 lines,
699 Path::new("file_path"),
742 Path::new("file_path"),
700 None,
743 None,
701 false,
744 false,
702 true,
745 true,
703 )
746 )
704 .unwrap()
747 .unwrap()
705 .0,
748 .0,
706 vec![IgnorePattern::new(
749 vec![IgnorePattern::new(
707 PatternSyntax::RelGlob,
750 PatternSyntax::RelGlob,
708 b"**.o",
751 b"**.o",
709 Path::new("file_path")
752 Path::new("file_path")
710 )]
753 )]
711 );
754 );
712 }
755 }
713
756
714 #[test]
757 #[test]
715 fn test_build_single_regex() {
758 fn test_build_single_regex() {
716 assert_eq!(
759 assert_eq!(
717 build_single_regex(
760 build_single_regex(
718 &IgnorePattern::new(
761 &IgnorePattern::new(
719 PatternSyntax::RelGlob,
762 PatternSyntax::RelGlob,
720 b"rust/target/",
763 b"rust/target/",
721 Path::new("")
764 Path::new("")
722 ),
765 ),
723 b"(?:/|$)"
766 b"(?:/|$)"
724 )
767 )
725 .unwrap(),
768 .unwrap(),
726 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
769 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
727 );
770 );
728 assert_eq!(
771 assert_eq!(
729 build_single_regex(
772 build_single_regex(
730 &IgnorePattern::new(
773 &IgnorePattern::new(
731 PatternSyntax::Regexp,
774 PatternSyntax::Regexp,
732 br"rust/target/\d+",
775 br"rust/target/\d+",
733 Path::new("")
776 Path::new("")
734 ),
777 ),
735 b"(?:/|$)"
778 b"(?:/|$)"
736 )
779 )
737 .unwrap(),
780 .unwrap(),
738 Some(br"rust/target/\d+".to_vec()),
781 Some(br"rust/target/\d+".to_vec()),
739 );
782 );
740 }
783 }
741
784
742 #[test]
785 #[test]
743 fn test_build_single_regex_shortcut() {
786 fn test_build_single_regex_shortcut() {
744 assert_eq!(
787 assert_eq!(
745 build_single_regex(
788 build_single_regex(
746 &IgnorePattern::new(
789 &IgnorePattern::new(
747 PatternSyntax::RootGlob,
790 PatternSyntax::RootGlob,
748 b"",
791 b"",
749 Path::new("")
792 Path::new("")
750 ),
793 ),
751 b"(?:/|$)"
794 b"(?:/|$)"
752 )
795 )
753 .unwrap(),
796 .unwrap(),
754 None,
797 None,
755 );
798 );
756 assert_eq!(
799 assert_eq!(
757 build_single_regex(
800 build_single_regex(
758 &IgnorePattern::new(
801 &IgnorePattern::new(
759 PatternSyntax::RootGlob,
802 PatternSyntax::RootGlob,
760 b"whatever",
803 b"whatever",
761 Path::new("")
804 Path::new("")
762 ),
805 ),
763 b"(?:/|$)"
806 b"(?:/|$)"
764 )
807 )
765 .unwrap(),
808 .unwrap(),
766 None,
809 None,
767 );
810 );
768 assert_eq!(
811 assert_eq!(
769 build_single_regex(
812 build_single_regex(
770 &IgnorePattern::new(
813 &IgnorePattern::new(
771 PatternSyntax::RootGlob,
814 PatternSyntax::RootGlob,
772 b"*.o",
815 b"*.o",
773 Path::new("")
816 Path::new("")
774 ),
817 ),
775 b"(?:/|$)"
818 b"(?:/|$)"
776 )
819 )
777 .unwrap(),
820 .unwrap(),
778 Some(br"[^/]*\.o(?:/|$)".to_vec()),
821 Some(br"[^/]*\.o(?:/|$)".to_vec()),
779 );
822 );
780 }
823 }
781
824
782 #[test]
825 #[test]
783 fn test_build_single_relregex() {
826 fn test_build_single_relregex() {
784 assert_eq!(
827 assert_eq!(
785 build_single_regex(
828 build_single_regex(
786 &IgnorePattern::new(
829 &IgnorePattern::new(
787 PatternSyntax::RelRegexp,
830 PatternSyntax::RelRegexp,
788 b"^ba{2}r",
831 b"^ba{2}r",
789 Path::new("")
832 Path::new("")
790 ),
833 ),
791 b"(?:/|$)"
834 b"(?:/|$)"
792 )
835 )
793 .unwrap(),
836 .unwrap(),
794 Some(b"^ba{2}r".to_vec()),
837 Some(b"^ba{2}r".to_vec()),
795 );
838 );
796 assert_eq!(
839 assert_eq!(
797 build_single_regex(
840 build_single_regex(
798 &IgnorePattern::new(
841 &IgnorePattern::new(
799 PatternSyntax::RelRegexp,
842 PatternSyntax::RelRegexp,
800 b"ba{2}r",
843 b"ba{2}r",
801 Path::new("")
844 Path::new("")
802 ),
845 ),
803 b"(?:/|$)"
846 b"(?:/|$)"
804 )
847 )
805 .unwrap(),
848 .unwrap(),
806 Some(b".*ba{2}r".to_vec()),
849 Some(b".*ba{2}r".to_vec()),
807 );
850 );
808 assert_eq!(
851 assert_eq!(
809 build_single_regex(
852 build_single_regex(
810 &IgnorePattern::new(
853 &IgnorePattern::new(
811 PatternSyntax::RelRegexp,
854 PatternSyntax::RelRegexp,
812 b"(?ia)ba{2}r",
855 b"(?ia)ba{2}r",
813 Path::new("")
856 Path::new("")
814 ),
857 ),
815 b"(?:/|$)"
858 b"(?:/|$)"
816 )
859 )
817 .unwrap(),
860 .unwrap(),
818 Some(b"(?ia:.*ba{2}r)".to_vec()),
861 Some(b"(?ia:.*ba{2}r)".to_vec()),
819 );
862 );
820 assert_eq!(
863 assert_eq!(
821 build_single_regex(
864 build_single_regex(
822 &IgnorePattern::new(
865 &IgnorePattern::new(
823 PatternSyntax::RelRegexp,
866 PatternSyntax::RelRegexp,
824 b"(?ia)^ba{2}r",
867 b"(?ia)^ba{2}r",
825 Path::new("")
868 Path::new("")
826 ),
869 ),
827 b"(?:/|$)"
870 b"(?:/|$)"
828 )
871 )
829 .unwrap(),
872 .unwrap(),
830 Some(b"(?ia:^ba{2}r)".to_vec()),
873 Some(b"(?ia:^ba{2}r)".to_vec()),
831 );
874 );
832 }
875 }
833 }
876 }
@@ -1,144 +1,144 b''
1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 // and Mercurial contributors
2 // and Mercurial contributors
3 //
3 //
4 // This software may be used and distributed according to the terms of the
4 // This software may be used and distributed according to the terms of the
5 // GNU General Public License version 2 or any later version.
5 // GNU General Public License version 2 or any later version.
6
6
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 narrow;
10 pub mod narrow;
11 pub mod sparse;
11 pub mod sparse;
12 pub use ancestors::{AncestorsIterator, MissingAncestors};
12 pub use ancestors::{AncestorsIterator, MissingAncestors};
13 pub mod dirstate;
13 pub mod dirstate;
14 pub mod dirstate_tree;
14 pub mod dirstate_tree;
15 pub mod discovery;
15 pub mod discovery;
16 pub mod exit_codes;
16 pub mod exit_codes;
17 pub mod requirements;
17 pub mod requirements;
18 pub mod testing; // unconditionally built, for use from integration tests
18 pub mod testing; // unconditionally built, for use from integration tests
19 pub use dirstate::{
19 pub use dirstate::{
20 dirs_multiset::{DirsMultiset, DirsMultisetIter},
20 dirs_multiset::{DirsMultiset, DirsMultisetIter},
21 status::{
21 status::{
22 BadMatch, BadType, DirstateStatus, HgPathCow, StatusError,
22 BadMatch, BadType, DirstateStatus, HgPathCow, StatusError,
23 StatusOptions,
23 StatusOptions,
24 },
24 },
25 DirstateEntry, DirstateParents, EntryState,
25 DirstateEntry, DirstateParents, EntryState,
26 };
26 };
27 pub mod copy_tracing;
27 pub mod copy_tracing;
28 mod filepatterns;
28 pub mod filepatterns;
29 pub mod matchers;
29 pub mod matchers;
30 pub mod repo;
30 pub mod repo;
31 pub mod revlog;
31 pub mod revlog;
32 pub use revlog::*;
32 pub use revlog::*;
33 pub mod checkexec;
33 pub mod checkexec;
34 pub mod config;
34 pub mod config;
35 pub mod lock;
35 pub mod lock;
36 pub mod logging;
36 pub mod logging;
37 pub mod operations;
37 pub mod operations;
38 pub mod revset;
38 pub mod revset;
39 pub mod utils;
39 pub mod utils;
40 pub mod vfs;
40 pub mod vfs;
41
41
42 use crate::utils::hg_path::{HgPathBuf, HgPathError};
42 use crate::utils::hg_path::{HgPathBuf, HgPathError};
43 pub use filepatterns::{
43 pub use filepatterns::{
44 parse_pattern_syntax, read_pattern_file, IgnorePattern,
44 parse_pattern_syntax, read_pattern_file, IgnorePattern,
45 PatternFileWarning, PatternSyntax,
45 PatternFileWarning, PatternSyntax,
46 };
46 };
47 use std::collections::HashMap;
47 use std::collections::HashMap;
48 use std::fmt;
48 use std::fmt;
49 use twox_hash::RandomXxHashBuilder64;
49 use twox_hash::RandomXxHashBuilder64;
50
50
51 pub type LineNumber = usize;
51 pub type LineNumber = usize;
52
52
53 /// Rust's default hasher is too slow because it tries to prevent collision
53 /// Rust's default hasher is too slow because it tries to prevent collision
54 /// attacks. We are not concerned about those: if an ill-minded person has
54 /// attacks. We are not concerned about those: if an ill-minded person has
55 /// write access to your repository, you have other issues.
55 /// write access to your repository, you have other issues.
56 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
56 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
57
57
58 // TODO: should this be the default `FastHashMap` for all of hg-core, not just
58 // TODO: should this be the default `FastHashMap` for all of hg-core, not just
59 // dirstate_tree? How does XxHash compare with AHash, hashbrown’s default?
59 // dirstate_tree? How does XxHash compare with AHash, hashbrown’s default?
60 pub type FastHashbrownMap<K, V> =
60 pub type FastHashbrownMap<K, V> =
61 hashbrown::HashMap<K, V, RandomXxHashBuilder64>;
61 hashbrown::HashMap<K, V, RandomXxHashBuilder64>;
62
62
63 #[derive(Debug, PartialEq)]
63 #[derive(Debug, PartialEq)]
64 pub enum DirstateMapError {
64 pub enum DirstateMapError {
65 PathNotFound(HgPathBuf),
65 PathNotFound(HgPathBuf),
66 InvalidPath(HgPathError),
66 InvalidPath(HgPathError),
67 }
67 }
68
68
69 impl From<HgPathError> for DirstateMapError {
69 impl From<HgPathError> for DirstateMapError {
70 fn from(error: HgPathError) -> Self {
70 fn from(error: HgPathError) -> Self {
71 Self::InvalidPath(error)
71 Self::InvalidPath(error)
72 }
72 }
73 }
73 }
74
74
75 impl fmt::Display for DirstateMapError {
75 impl fmt::Display for DirstateMapError {
76 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77 match self {
77 match self {
78 DirstateMapError::PathNotFound(_) => {
78 DirstateMapError::PathNotFound(_) => {
79 f.write_str("expected a value, found none")
79 f.write_str("expected a value, found none")
80 }
80 }
81 DirstateMapError::InvalidPath(path_error) => path_error.fmt(f),
81 DirstateMapError::InvalidPath(path_error) => path_error.fmt(f),
82 }
82 }
83 }
83 }
84 }
84 }
85
85
86 #[derive(Debug, derive_more::From)]
86 #[derive(Debug, derive_more::From)]
87 pub enum DirstateError {
87 pub enum DirstateError {
88 Map(DirstateMapError),
88 Map(DirstateMapError),
89 Common(errors::HgError),
89 Common(errors::HgError),
90 }
90 }
91
91
92 impl From<HgPathError> for DirstateError {
92 impl From<HgPathError> for DirstateError {
93 fn from(error: HgPathError) -> Self {
93 fn from(error: HgPathError) -> Self {
94 Self::Map(DirstateMapError::InvalidPath(error))
94 Self::Map(DirstateMapError::InvalidPath(error))
95 }
95 }
96 }
96 }
97
97
98 impl fmt::Display for DirstateError {
98 impl fmt::Display for DirstateError {
99 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100 match self {
100 match self {
101 DirstateError::Map(error) => error.fmt(f),
101 DirstateError::Map(error) => error.fmt(f),
102 DirstateError::Common(error) => error.fmt(f),
102 DirstateError::Common(error) => error.fmt(f),
103 }
103 }
104 }
104 }
105 }
105 }
106
106
107 #[derive(Debug, derive_more::From)]
107 #[derive(Debug, derive_more::From)]
108 pub enum PatternError {
108 pub enum PatternError {
109 #[from]
109 #[from]
110 Path(HgPathError),
110 Path(HgPathError),
111 UnsupportedSyntax(String),
111 UnsupportedSyntax(String),
112 UnsupportedSyntaxInFile(String, String, usize),
112 UnsupportedSyntaxInFile(String, String, usize),
113 TooLong(usize),
113 TooLong(usize),
114 #[from]
114 #[from]
115 IO(std::io::Error),
115 IO(std::io::Error),
116 /// Needed a pattern that can be turned into a regex but got one that
116 /// Needed a pattern that can be turned into a regex but got one that
117 /// can't. This should only happen through programmer error.
117 /// can't. This should only happen through programmer error.
118 NonRegexPattern(IgnorePattern),
118 NonRegexPattern(IgnorePattern),
119 }
119 }
120
120
121 impl fmt::Display for PatternError {
121 impl fmt::Display for PatternError {
122 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123 match self {
123 match self {
124 PatternError::UnsupportedSyntax(syntax) => {
124 PatternError::UnsupportedSyntax(syntax) => {
125 write!(f, "Unsupported syntax {}", syntax)
125 write!(f, "Unsupported syntax {}", syntax)
126 }
126 }
127 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
127 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
128 write!(
128 write!(
129 f,
129 f,
130 "{}:{}: unsupported syntax {}",
130 "{}:{}: unsupported syntax {}",
131 file_path, line, syntax
131 file_path, line, syntax
132 )
132 )
133 }
133 }
134 PatternError::TooLong(size) => {
134 PatternError::TooLong(size) => {
135 write!(f, "matcher pattern is too long ({} bytes)", size)
135 write!(f, "matcher pattern is too long ({} bytes)", size)
136 }
136 }
137 PatternError::IO(error) => error.fmt(f),
137 PatternError::IO(error) => error.fmt(f),
138 PatternError::Path(error) => error.fmt(f),
138 PatternError::Path(error) => error.fmt(f),
139 PatternError::NonRegexPattern(pattern) => {
139 PatternError::NonRegexPattern(pattern) => {
140 write!(f, "'{:?}' cannot be turned into a regex", pattern)
140 write!(f, "'{:?}' cannot be turned into a regex", pattern)
141 }
141 }
142 }
142 }
143 }
143 }
144 }
144 }
@@ -1,689 +1,718 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 use crate::error::CommandError;
8 use crate::error::CommandError;
9 use crate::ui::{
9 use crate::ui::{
10 format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths,
10 format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths,
11 RelativePaths, Ui,
11 RelativePaths, Ui,
12 };
12 };
13 use crate::utils::path_utils::RelativizePaths;
13 use crate::utils::path_utils::RelativizePaths;
14 use clap::Arg;
14 use clap::Arg;
15 use format_bytes::format_bytes;
15 use format_bytes::format_bytes;
16 use hg::config::Config;
16 use hg::config::Config;
17 use hg::dirstate::has_exec_bit;
17 use hg::dirstate::has_exec_bit;
18 use hg::dirstate::status::StatusPath;
18 use hg::dirstate::status::StatusPath;
19 use hg::dirstate::TruncatedTimestamp;
19 use hg::dirstate::TruncatedTimestamp;
20 use hg::errors::{HgError, IoResultExt};
20 use hg::errors::{HgError, IoResultExt};
21 use hg::filepatterns::parse_pattern_args;
21 use hg::lock::LockError;
22 use hg::lock::LockError;
22 use hg::manifest::Manifest;
23 use hg::manifest::Manifest;
23 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
24 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
24 use hg::repo::Repo;
25 use hg::repo::Repo;
25 use hg::utils::debug::debug_wait_for_file;
26 use hg::utils::debug::debug_wait_for_file;
26 use hg::utils::files::get_bytes_from_os_string;
27 use hg::utils::files::{
27 use hg::utils::files::get_path_from_bytes;
28 get_bytes_from_os_str, get_bytes_from_os_string, get_path_from_bytes,
29 };
28 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
30 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
29 use hg::DirstateStatus;
31 use hg::DirstateStatus;
30 use hg::PatternFileWarning;
32 use hg::PatternFileWarning;
31 use hg::StatusError;
33 use hg::StatusError;
32 use hg::StatusOptions;
34 use hg::StatusOptions;
33 use hg::{self, narrow, sparse};
35 use hg::{self, narrow, sparse};
34 use log::info;
36 use log::info;
35 use rayon::prelude::*;
37 use rayon::prelude::*;
36 use std::io;
38 use std::io;
37 use std::path::PathBuf;
39 use std::path::PathBuf;
38
40
39 pub const HELP_TEXT: &str = "
41 pub const HELP_TEXT: &str = "
40 Show changed files in the working directory
42 Show changed files in the working directory
41
43
42 This is a pure Rust version of `hg status`.
44 This is a pure Rust version of `hg status`.
43
45
44 Some options might be missing, check the list below.
46 Some options might be missing, check the list below.
45 ";
47 ";
46
48
47 pub fn args() -> clap::Command {
49 pub fn args() -> clap::Command {
48 clap::command!("status")
50 clap::command!("status")
49 .alias("st")
51 .alias("st")
50 .about(HELP_TEXT)
52 .about(HELP_TEXT)
51 .arg(
53 .arg(
54 Arg::new("file")
55 .value_parser(clap::value_parser!(std::ffi::OsString))
56 .help("show only these files")
57 .action(clap::ArgAction::Append),
58 )
59 .arg(
52 Arg::new("all")
60 Arg::new("all")
53 .help("show status of all files")
61 .help("show status of all files")
54 .short('A')
62 .short('A')
55 .action(clap::ArgAction::SetTrue)
63 .action(clap::ArgAction::SetTrue)
56 .long("all"),
64 .long("all"),
57 )
65 )
58 .arg(
66 .arg(
59 Arg::new("modified")
67 Arg::new("modified")
60 .help("show only modified files")
68 .help("show only modified files")
61 .short('m')
69 .short('m')
62 .action(clap::ArgAction::SetTrue)
70 .action(clap::ArgAction::SetTrue)
63 .long("modified"),
71 .long("modified"),
64 )
72 )
65 .arg(
73 .arg(
66 Arg::new("added")
74 Arg::new("added")
67 .help("show only added files")
75 .help("show only added files")
68 .short('a')
76 .short('a')
69 .action(clap::ArgAction::SetTrue)
77 .action(clap::ArgAction::SetTrue)
70 .long("added"),
78 .long("added"),
71 )
79 )
72 .arg(
80 .arg(
73 Arg::new("removed")
81 Arg::new("removed")
74 .help("show only removed files")
82 .help("show only removed files")
75 .short('r')
83 .short('r')
76 .action(clap::ArgAction::SetTrue)
84 .action(clap::ArgAction::SetTrue)
77 .long("removed"),
85 .long("removed"),
78 )
86 )
79 .arg(
87 .arg(
80 Arg::new("clean")
88 Arg::new("clean")
81 .help("show only clean files")
89 .help("show only clean files")
82 .short('c')
90 .short('c')
83 .action(clap::ArgAction::SetTrue)
91 .action(clap::ArgAction::SetTrue)
84 .long("clean"),
92 .long("clean"),
85 )
93 )
86 .arg(
94 .arg(
87 Arg::new("deleted")
95 Arg::new("deleted")
88 .help("show only deleted files")
96 .help("show only deleted files")
89 .short('d')
97 .short('d')
90 .action(clap::ArgAction::SetTrue)
98 .action(clap::ArgAction::SetTrue)
91 .long("deleted"),
99 .long("deleted"),
92 )
100 )
93 .arg(
101 .arg(
94 Arg::new("unknown")
102 Arg::new("unknown")
95 .help("show only unknown (not tracked) files")
103 .help("show only unknown (not tracked) files")
96 .short('u')
104 .short('u')
97 .action(clap::ArgAction::SetTrue)
105 .action(clap::ArgAction::SetTrue)
98 .long("unknown"),
106 .long("unknown"),
99 )
107 )
100 .arg(
108 .arg(
101 Arg::new("ignored")
109 Arg::new("ignored")
102 .help("show only ignored files")
110 .help("show only ignored files")
103 .short('i')
111 .short('i')
104 .action(clap::ArgAction::SetTrue)
112 .action(clap::ArgAction::SetTrue)
105 .long("ignored"),
113 .long("ignored"),
106 )
114 )
107 .arg(
115 .arg(
108 Arg::new("copies")
116 Arg::new("copies")
109 .help("show source of copied files (DEFAULT: ui.statuscopies)")
117 .help("show source of copied files (DEFAULT: ui.statuscopies)")
110 .short('C')
118 .short('C')
111 .action(clap::ArgAction::SetTrue)
119 .action(clap::ArgAction::SetTrue)
112 .long("copies"),
120 .long("copies"),
113 )
121 )
114 .arg(
122 .arg(
115 Arg::new("print0")
123 Arg::new("print0")
116 .help("end filenames with NUL, for use with xargs")
124 .help("end filenames with NUL, for use with xargs")
117 .short('0')
125 .short('0')
118 .action(clap::ArgAction::SetTrue)
126 .action(clap::ArgAction::SetTrue)
119 .long("print0"),
127 .long("print0"),
120 )
128 )
121 .arg(
129 .arg(
122 Arg::new("no-status")
130 Arg::new("no-status")
123 .help("hide status prefix")
131 .help("hide status prefix")
124 .short('n')
132 .short('n')
125 .action(clap::ArgAction::SetTrue)
133 .action(clap::ArgAction::SetTrue)
126 .long("no-status"),
134 .long("no-status"),
127 )
135 )
128 .arg(
136 .arg(
129 Arg::new("verbose")
137 Arg::new("verbose")
130 .help("enable additional output")
138 .help("enable additional output")
131 .short('v')
139 .short('v')
132 .action(clap::ArgAction::SetTrue)
140 .action(clap::ArgAction::SetTrue)
133 .long("verbose"),
141 .long("verbose"),
134 )
142 )
135 }
143 }
136
144
137 /// Pure data type allowing the caller to specify file states to display
145 /// Pure data type allowing the caller to specify file states to display
138 #[derive(Copy, Clone, Debug)]
146 #[derive(Copy, Clone, Debug)]
139 pub struct DisplayStates {
147 pub struct DisplayStates {
140 pub modified: bool,
148 pub modified: bool,
141 pub added: bool,
149 pub added: bool,
142 pub removed: bool,
150 pub removed: bool,
143 pub clean: bool,
151 pub clean: bool,
144 pub deleted: bool,
152 pub deleted: bool,
145 pub unknown: bool,
153 pub unknown: bool,
146 pub ignored: bool,
154 pub ignored: bool,
147 }
155 }
148
156
149 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
157 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
150 modified: true,
158 modified: true,
151 added: true,
159 added: true,
152 removed: true,
160 removed: true,
153 clean: false,
161 clean: false,
154 deleted: true,
162 deleted: true,
155 unknown: true,
163 unknown: true,
156 ignored: false,
164 ignored: false,
157 };
165 };
158
166
159 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
167 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
160 modified: true,
168 modified: true,
161 added: true,
169 added: true,
162 removed: true,
170 removed: true,
163 clean: true,
171 clean: true,
164 deleted: true,
172 deleted: true,
165 unknown: true,
173 unknown: true,
166 ignored: true,
174 ignored: true,
167 };
175 };
168
176
169 impl DisplayStates {
177 impl DisplayStates {
170 pub fn is_empty(&self) -> bool {
178 pub fn is_empty(&self) -> bool {
171 !(self.modified
179 !(self.modified
172 || self.added
180 || self.added
173 || self.removed
181 || self.removed
174 || self.clean
182 || self.clean
175 || self.deleted
183 || self.deleted
176 || self.unknown
184 || self.unknown
177 || self.ignored)
185 || self.ignored)
178 }
186 }
179 }
187 }
180
188
181 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
189 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
182 Ok(repo.dirstate_parents()?.is_merge())
190 Ok(repo.dirstate_parents()?.is_merge())
183 }
191 }
184
192
185 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
193 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
186 // These are all the known values for the [fname] argument of
194 // These are all the known values for the [fname] argument of
187 // [addunfinished] function in [state.py]
195 // [addunfinished] function in [state.py]
188 let known_state_files: &[&str] = &[
196 let known_state_files: &[&str] = &[
189 "bisect.state",
197 "bisect.state",
190 "graftstate",
198 "graftstate",
191 "histedit-state",
199 "histedit-state",
192 "rebasestate",
200 "rebasestate",
193 "shelvedstate",
201 "shelvedstate",
194 "transplant/journal",
202 "transplant/journal",
195 "updatestate",
203 "updatestate",
196 ];
204 ];
197 if has_unfinished_merge(repo)? {
205 if has_unfinished_merge(repo)? {
198 return Ok(true);
206 return Ok(true);
199 };
207 };
200 for f in known_state_files {
208 for f in known_state_files {
201 if repo.hg_vfs().join(f).exists() {
209 if repo.hg_vfs().join(f).exists() {
202 return Ok(true);
210 return Ok(true);
203 }
211 }
204 }
212 }
205 Ok(false)
213 Ok(false)
206 }
214 }
207
215
208 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
216 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
209 // TODO: lift these limitations
217 // TODO: lift these limitations
210 if invocation
218 if invocation
211 .config
219 .config
212 .get(b"commands", b"status.terse")
220 .get(b"commands", b"status.terse")
213 .is_some()
221 .is_some()
214 {
222 {
215 return Err(CommandError::unsupported(
223 return Err(CommandError::unsupported(
216 "status.terse is not yet supported with rhg status",
224 "status.terse is not yet supported with rhg status",
217 ));
225 ));
218 }
226 }
219
227
220 let ui = invocation.ui;
228 let ui = invocation.ui;
221 let config = invocation.config;
229 let config = invocation.config;
222 let args = invocation.subcommand_args;
230 let args = invocation.subcommand_args;
223
231
224 let print0 = args.get_flag("print0");
232 let print0 = args.get_flag("print0");
225 let verbose = args.get_flag("verbose")
233 let verbose = args.get_flag("verbose")
226 || config.get_bool(b"ui", b"verbose")?
234 || config.get_bool(b"ui", b"verbose")?
227 || config.get_bool(b"commands", b"status.verbose")?;
235 || config.get_bool(b"commands", b"status.verbose")?;
228 let verbose = verbose && !print0;
236 let verbose = verbose && !print0;
229
237
230 let all = args.get_flag("all");
238 let all = args.get_flag("all");
231 let display_states = if all {
239 let display_states = if all {
232 // TODO when implementing `--quiet`: it excludes clean files
240 // TODO when implementing `--quiet`: it excludes clean files
233 // from `--all`
241 // from `--all`
234 ALL_DISPLAY_STATES
242 ALL_DISPLAY_STATES
235 } else {
243 } else {
236 let requested = DisplayStates {
244 let requested = DisplayStates {
237 modified: args.get_flag("modified"),
245 modified: args.get_flag("modified"),
238 added: args.get_flag("added"),
246 added: args.get_flag("added"),
239 removed: args.get_flag("removed"),
247 removed: args.get_flag("removed"),
240 clean: args.get_flag("clean"),
248 clean: args.get_flag("clean"),
241 deleted: args.get_flag("deleted"),
249 deleted: args.get_flag("deleted"),
242 unknown: args.get_flag("unknown"),
250 unknown: args.get_flag("unknown"),
243 ignored: args.get_flag("ignored"),
251 ignored: args.get_flag("ignored"),
244 };
252 };
245 if requested.is_empty() {
253 if requested.is_empty() {
246 DEFAULT_DISPLAY_STATES
254 DEFAULT_DISPLAY_STATES
247 } else {
255 } else {
248 requested
256 requested
249 }
257 }
250 };
258 };
251 let no_status = args.get_flag("no-status");
259 let no_status = args.get_flag("no-status");
252 let list_copies = all
260 let list_copies = all
253 || args.get_flag("copies")
261 || args.get_flag("copies")
254 || config.get_bool(b"ui", b"statuscopies")?;
262 || config.get_bool(b"ui", b"statuscopies")?;
255
263
256 let repo = invocation.repo?;
264 let repo = invocation.repo?;
257
265
258 if verbose && has_unfinished_state(repo)? {
266 if verbose && has_unfinished_state(repo)? {
259 return Err(CommandError::unsupported(
267 return Err(CommandError::unsupported(
260 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
268 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
261 ));
269 ));
262 }
270 }
263
271
264 let mut dmap = repo.dirstate_map_mut()?;
272 let mut dmap = repo.dirstate_map_mut()?;
265
273
266 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
274 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
267
275
268 let options = StatusOptions {
276 let options = StatusOptions {
269 check_exec,
277 check_exec,
270 list_clean: display_states.clean,
278 list_clean: display_states.clean,
271 list_unknown: display_states.unknown,
279 list_unknown: display_states.unknown,
272 list_ignored: display_states.ignored,
280 list_ignored: display_states.ignored,
273 list_copies,
281 list_copies,
274 collect_traversed_dirs: false,
282 collect_traversed_dirs: false,
275 };
283 };
276
284
277 type StatusResult<'a> =
285 type StatusResult<'a> =
278 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
286 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
279
287
280 let after_status = |res: StatusResult| -> Result<_, CommandError> {
288 let after_status = |res: StatusResult| -> Result<_, CommandError> {
281 let (mut ds_status, pattern_warnings) = res?;
289 let (mut ds_status, pattern_warnings) = res?;
282 for warning in pattern_warnings {
290 for warning in pattern_warnings {
283 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
291 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
284 }
292 }
285
293
286 for (path, error) in ds_status.bad {
294 for (path, error) in ds_status.bad {
287 let error = match error {
295 let error = match error {
288 hg::BadMatch::OsError(code) => {
296 hg::BadMatch::OsError(code) => {
289 std::io::Error::from_raw_os_error(code).to_string()
297 std::io::Error::from_raw_os_error(code).to_string()
290 }
298 }
291 hg::BadMatch::BadType(ty) => {
299 hg::BadMatch::BadType(ty) => {
292 format!("unsupported file type (type is {})", ty)
300 format!("unsupported file type (type is {})", ty)
293 }
301 }
294 };
302 };
295 ui.write_stderr(&format_bytes!(
303 ui.write_stderr(&format_bytes!(
296 b"{}: {}\n",
304 b"{}: {}\n",
297 path.as_bytes(),
305 path.as_bytes(),
298 error.as_bytes()
306 error.as_bytes()
299 ))?
307 ))?
300 }
308 }
301 if !ds_status.unsure.is_empty() {
309 if !ds_status.unsure.is_empty() {
302 info!(
310 info!(
303 "Files to be rechecked by retrieval from filelog: {:?}",
311 "Files to be rechecked by retrieval from filelog: {:?}",
304 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
312 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
305 );
313 );
306 }
314 }
307 let mut fixup = Vec::new();
315 let mut fixup = Vec::new();
308 if !ds_status.unsure.is_empty()
316 if !ds_status.unsure.is_empty()
309 && (display_states.modified || display_states.clean)
317 && (display_states.modified || display_states.clean)
310 {
318 {
311 let p1 = repo.dirstate_parents()?.p1;
319 let p1 = repo.dirstate_parents()?.p1;
312 let manifest = repo.manifest_for_node(p1).map_err(|e| {
320 let manifest = repo.manifest_for_node(p1).map_err(|e| {
313 CommandError::from((e, &*format!("{:x}", p1.short())))
321 CommandError::from((e, &*format!("{:x}", p1.short())))
314 })?;
322 })?;
315 let working_directory_vfs = repo.working_directory_vfs();
323 let working_directory_vfs = repo.working_directory_vfs();
316 let store_vfs = repo.store_vfs();
324 let store_vfs = repo.store_vfs();
317 let res: Vec<_> = ds_status
325 let res: Vec<_> = ds_status
318 .unsure
326 .unsure
319 .into_par_iter()
327 .into_par_iter()
320 .map(|to_check| {
328 .map(|to_check| {
321 // The compiler seems to get a bit confused with complex
329 // The compiler seems to get a bit confused with complex
322 // inference when using a parallel iterator + map
330 // inference when using a parallel iterator + map
323 // + map_err + collect, so let's just inline some of the
331 // + map_err + collect, so let's just inline some of the
324 // logic.
332 // logic.
325 match unsure_is_modified(
333 match unsure_is_modified(
326 working_directory_vfs,
334 working_directory_vfs,
327 store_vfs,
335 store_vfs,
328 check_exec,
336 check_exec,
329 &manifest,
337 &manifest,
330 &to_check.path,
338 &to_check.path,
331 ) {
339 ) {
332 Err(HgError::IoError { .. }) => {
340 Err(HgError::IoError { .. }) => {
333 // IO errors most likely stem from the file being
341 // IO errors most likely stem from the file being
334 // deleted even though we know it's in the
342 // deleted even though we know it's in the
335 // dirstate.
343 // dirstate.
336 Ok((to_check, UnsureOutcome::Deleted))
344 Ok((to_check, UnsureOutcome::Deleted))
337 }
345 }
338 Ok(outcome) => Ok((to_check, outcome)),
346 Ok(outcome) => Ok((to_check, outcome)),
339 Err(e) => Err(e),
347 Err(e) => Err(e),
340 }
348 }
341 })
349 })
342 .collect::<Result<_, _>>()?;
350 .collect::<Result<_, _>>()?;
343 for (status_path, outcome) in res.into_iter() {
351 for (status_path, outcome) in res.into_iter() {
344 match outcome {
352 match outcome {
345 UnsureOutcome::Clean => {
353 UnsureOutcome::Clean => {
346 if display_states.clean {
354 if display_states.clean {
347 ds_status.clean.push(status_path.clone());
355 ds_status.clean.push(status_path.clone());
348 }
356 }
349 fixup.push(status_path.path.into_owned())
357 fixup.push(status_path.path.into_owned())
350 }
358 }
351 UnsureOutcome::Modified => {
359 UnsureOutcome::Modified => {
352 if display_states.modified {
360 if display_states.modified {
353 ds_status.modified.push(status_path);
361 ds_status.modified.push(status_path);
354 }
362 }
355 }
363 }
356 UnsureOutcome::Deleted => {
364 UnsureOutcome::Deleted => {
357 if display_states.deleted {
365 if display_states.deleted {
358 ds_status.deleted.push(status_path);
366 ds_status.deleted.push(status_path);
359 }
367 }
360 }
368 }
361 }
369 }
362 }
370 }
363 }
371 }
364
372
365 let relative_status = config
373 let relative_status = config
366 .get_option(b"commands", b"status.relative")?
374 .get_option(b"commands", b"status.relative")?
367 .expect("commands.status.relative should have a default value");
375 .expect("commands.status.relative should have a default value");
368
376
369 let relativize_paths = relative_status || {
377 let relativize_paths = relative_status || {
370 // TODO should be dependent on whether patterns are passed once
371 // we support those.
372 // See in Python code with `getuipathfn` usage in `commands.py`.
378 // See in Python code with `getuipathfn` usage in `commands.py`.
373 let legacy_relative_behavior = false;
379 let legacy_relative_behavior = args.contains_id("file");
374 match relative_paths(invocation.config)? {
380 match relative_paths(invocation.config)? {
375 RelativePaths::Legacy => legacy_relative_behavior,
381 RelativePaths::Legacy => legacy_relative_behavior,
376 RelativePaths::Bool(v) => v,
382 RelativePaths::Bool(v) => v,
377 }
383 }
378 };
384 };
379
385
380 let output = DisplayStatusPaths {
386 let output = DisplayStatusPaths {
381 ui,
387 ui,
382 no_status,
388 no_status,
383 relativize: if relativize_paths {
389 relativize: if relativize_paths {
384 Some(RelativizePaths::new(repo)?)
390 Some(RelativizePaths::new(repo)?)
385 } else {
391 } else {
386 None
392 None
387 },
393 },
388 print0,
394 print0,
389 };
395 };
390 if display_states.modified {
396 if display_states.modified {
391 output.display(b"M ", "status.modified", ds_status.modified)?;
397 output.display(b"M ", "status.modified", ds_status.modified)?;
392 }
398 }
393 if display_states.added {
399 if display_states.added {
394 output.display(b"A ", "status.added", ds_status.added)?;
400 output.display(b"A ", "status.added", ds_status.added)?;
395 }
401 }
396 if display_states.removed {
402 if display_states.removed {
397 output.display(b"R ", "status.removed", ds_status.removed)?;
403 output.display(b"R ", "status.removed", ds_status.removed)?;
398 }
404 }
399 if display_states.deleted {
405 if display_states.deleted {
400 output.display(b"! ", "status.deleted", ds_status.deleted)?;
406 output.display(b"! ", "status.deleted", ds_status.deleted)?;
401 }
407 }
402 if display_states.unknown {
408 if display_states.unknown {
403 output.display(b"? ", "status.unknown", ds_status.unknown)?;
409 output.display(b"? ", "status.unknown", ds_status.unknown)?;
404 }
410 }
405 if display_states.ignored {
411 if display_states.ignored {
406 output.display(b"I ", "status.ignored", ds_status.ignored)?;
412 output.display(b"I ", "status.ignored", ds_status.ignored)?;
407 }
413 }
408 if display_states.clean {
414 if display_states.clean {
409 output.display(b"C ", "status.clean", ds_status.clean)?;
415 output.display(b"C ", "status.clean", ds_status.clean)?;
410 }
416 }
411
417
412 let dirstate_write_needed = ds_status.dirty;
418 let dirstate_write_needed = ds_status.dirty;
413 let filesystem_time_at_status_start =
419 let filesystem_time_at_status_start =
414 ds_status.filesystem_time_at_status_start;
420 ds_status.filesystem_time_at_status_start;
415
421
416 Ok((
422 Ok((
417 fixup,
423 fixup,
418 dirstate_write_needed,
424 dirstate_write_needed,
419 filesystem_time_at_status_start,
425 filesystem_time_at_status_start,
420 ))
426 ))
421 };
427 };
422 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
428 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
423 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
429 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
424 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
430 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
425 (true, true) => {
431 (true, true) => {
426 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
432 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
427 }
433 }
428 (true, false) => narrow_matcher,
434 (true, false) => narrow_matcher,
429 (false, true) => sparse_matcher,
435 (false, true) => sparse_matcher,
430 (false, false) => Box::new(AlwaysMatcher),
436 (false, false) => Box::new(AlwaysMatcher),
431 };
437 };
438 let matcher = match args.get_many::<std::ffi::OsString>("file") {
439 None => matcher,
440 Some(files) => {
441 let patterns: Vec<Vec<u8>> = files
442 .filter(|s| !s.is_empty())
443 .map(get_bytes_from_os_str)
444 .collect();
445 for file in &patterns {
446 if file.starts_with(b"set:") {
447 return Err(CommandError::unsupported("fileset"));
448 }
449 }
450 let cwd = hg::utils::current_dir()?;
451 let root = repo.working_directory_path();
452 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
453 let files_matcher =
454 hg::matchers::PatternMatcher::new(ignore_patterns)?;
455 Box::new(IntersectionMatcher::new(
456 Box::new(files_matcher),
457 matcher,
458 ))
459 }
460 };
432
461
433 print_narrow_sparse_warnings(
462 print_narrow_sparse_warnings(
434 &narrow_warnings,
463 &narrow_warnings,
435 &sparse_warnings,
464 &sparse_warnings,
436 ui,
465 ui,
437 repo,
466 repo,
438 )?;
467 )?;
439 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
468 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
440 dmap.with_status(
469 dmap.with_status(
441 matcher.as_ref(),
470 matcher.as_ref(),
442 repo.working_directory_path().to_owned(),
471 repo.working_directory_path().to_owned(),
443 ignore_files(repo, config),
472 ignore_files(repo, config),
444 options,
473 options,
445 after_status,
474 after_status,
446 )?;
475 )?;
447
476
448 // Development config option to test write races
477 // Development config option to test write races
449 if let Err(e) =
478 if let Err(e) =
450 debug_wait_for_file(config, "status.pre-dirstate-write-file")
479 debug_wait_for_file(config, "status.pre-dirstate-write-file")
451 {
480 {
452 ui.write_stderr(e.as_bytes()).ok();
481 ui.write_stderr(e.as_bytes()).ok();
453 }
482 }
454
483
455 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
484 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
456 && !dirstate_write_needed
485 && !dirstate_write_needed
457 {
486 {
458 // Nothing to update
487 // Nothing to update
459 return Ok(());
488 return Ok(());
460 }
489 }
461
490
462 // Update the dirstate on disk if we can
491 // Update the dirstate on disk if we can
463 let with_lock_result =
492 let with_lock_result =
464 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
493 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
465 if let Some(mtime_boundary) = filesystem_time_at_status_start {
494 if let Some(mtime_boundary) = filesystem_time_at_status_start {
466 for hg_path in fixup {
495 for hg_path in fixup {
467 use std::os::unix::fs::MetadataExt;
496 use std::os::unix::fs::MetadataExt;
468 let fs_path = hg_path_to_path_buf(&hg_path)
497 let fs_path = hg_path_to_path_buf(&hg_path)
469 .expect("HgPath conversion");
498 .expect("HgPath conversion");
470 // Specifically do not reuse `fs_metadata` from
499 // Specifically do not reuse `fs_metadata` from
471 // `unsure_is_clean` which was needed before reading
500 // `unsure_is_clean` which was needed before reading
472 // contents. Here we access metadata again after reading
501 // contents. Here we access metadata again after reading
473 // content, in case it changed in the meantime.
502 // content, in case it changed in the meantime.
474 let metadata_res = repo
503 let metadata_res = repo
475 .working_directory_vfs()
504 .working_directory_vfs()
476 .symlink_metadata(&fs_path);
505 .symlink_metadata(&fs_path);
477 let fs_metadata = match metadata_res {
506 let fs_metadata = match metadata_res {
478 Ok(meta) => meta,
507 Ok(meta) => meta,
479 Err(err) => match err {
508 Err(err) => match err {
480 HgError::IoError { .. } => {
509 HgError::IoError { .. } => {
481 // The file has probably been deleted. In any
510 // The file has probably been deleted. In any
482 // case, it was in the dirstate before, so
511 // case, it was in the dirstate before, so
483 // let's ignore the error.
512 // let's ignore the error.
484 continue;
513 continue;
485 }
514 }
486 _ => return Err(err.into()),
515 _ => return Err(err.into()),
487 },
516 },
488 };
517 };
489 if let Some(mtime) =
518 if let Some(mtime) =
490 TruncatedTimestamp::for_reliable_mtime_of(
519 TruncatedTimestamp::for_reliable_mtime_of(
491 &fs_metadata,
520 &fs_metadata,
492 &mtime_boundary,
521 &mtime_boundary,
493 )
522 )
494 .when_reading_file(&fs_path)?
523 .when_reading_file(&fs_path)?
495 {
524 {
496 let mode = fs_metadata.mode();
525 let mode = fs_metadata.mode();
497 let size = fs_metadata.len();
526 let size = fs_metadata.len();
498 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
527 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
499 dirstate_write_needed = true
528 dirstate_write_needed = true
500 }
529 }
501 }
530 }
502 }
531 }
503 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
532 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
504 if dirstate_write_needed {
533 if dirstate_write_needed {
505 repo.write_dirstate()?
534 repo.write_dirstate()?
506 }
535 }
507 Ok(())
536 Ok(())
508 });
537 });
509 match with_lock_result {
538 match with_lock_result {
510 Ok(closure_result) => closure_result?,
539 Ok(closure_result) => closure_result?,
511 Err(LockError::AlreadyHeld) => {
540 Err(LockError::AlreadyHeld) => {
512 // Not updating the dirstate is not ideal but not critical:
541 // Not updating the dirstate is not ideal but not critical:
513 // don’t keep our caller waiting until some other Mercurial
542 // don’t keep our caller waiting until some other Mercurial
514 // process releases the lock.
543 // process releases the lock.
515 log::info!("not writing dirstate from `status`: lock is held")
544 log::info!("not writing dirstate from `status`: lock is held")
516 }
545 }
517 Err(LockError::Other(HgError::IoError { error, .. }))
546 Err(LockError::Other(HgError::IoError { error, .. }))
518 if error.kind() == io::ErrorKind::PermissionDenied =>
547 if error.kind() == io::ErrorKind::PermissionDenied =>
519 {
548 {
520 // `hg status` on a read-only repository is fine
549 // `hg status` on a read-only repository is fine
521 }
550 }
522 Err(LockError::Other(error)) => {
551 Err(LockError::Other(error)) => {
523 // Report other I/O errors
552 // Report other I/O errors
524 Err(error)?
553 Err(error)?
525 }
554 }
526 }
555 }
527 Ok(())
556 Ok(())
528 }
557 }
529
558
530 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
559 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
531 let mut ignore_files = Vec::new();
560 let mut ignore_files = Vec::new();
532 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
561 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
533 if repo_ignore.exists() {
562 if repo_ignore.exists() {
534 ignore_files.push(repo_ignore)
563 ignore_files.push(repo_ignore)
535 }
564 }
536 for (key, value) in config.iter_section(b"ui") {
565 for (key, value) in config.iter_section(b"ui") {
537 if key == b"ignore" || key.starts_with(b"ignore.") {
566 if key == b"ignore" || key.starts_with(b"ignore.") {
538 let path = get_path_from_bytes(value);
567 let path = get_path_from_bytes(value);
539 // TODO: expand "~/" and environment variable here, like Python
568 // TODO: expand "~/" and environment variable here, like Python
540 // does with `os.path.expanduser` and `os.path.expandvars`
569 // does with `os.path.expanduser` and `os.path.expandvars`
541
570
542 let joined = repo.working_directory_path().join(path);
571 let joined = repo.working_directory_path().join(path);
543 ignore_files.push(joined);
572 ignore_files.push(joined);
544 }
573 }
545 }
574 }
546 ignore_files
575 ignore_files
547 }
576 }
548
577
549 struct DisplayStatusPaths<'a> {
578 struct DisplayStatusPaths<'a> {
550 ui: &'a Ui,
579 ui: &'a Ui,
551 no_status: bool,
580 no_status: bool,
552 relativize: Option<RelativizePaths>,
581 relativize: Option<RelativizePaths>,
553 print0: bool,
582 print0: bool,
554 }
583 }
555
584
556 impl DisplayStatusPaths<'_> {
585 impl DisplayStatusPaths<'_> {
557 // Probably more elegant to use a Deref or Borrow trait rather than
586 // Probably more elegant to use a Deref or Borrow trait rather than
558 // harcode HgPathBuf, but probably not really useful at this point
587 // harcode HgPathBuf, but probably not really useful at this point
559 fn display(
588 fn display(
560 &self,
589 &self,
561 status_prefix: &[u8],
590 status_prefix: &[u8],
562 label: &'static str,
591 label: &'static str,
563 mut paths: Vec<StatusPath<'_>>,
592 mut paths: Vec<StatusPath<'_>>,
564 ) -> Result<(), CommandError> {
593 ) -> Result<(), CommandError> {
565 paths.sort_unstable();
594 paths.sort_unstable();
566 // TODO: get the stdout lock once for the whole loop
595 // TODO: get the stdout lock once for the whole loop
567 // instead of in each write
596 // instead of in each write
568 for StatusPath { path, copy_source } in paths {
597 for StatusPath { path, copy_source } in paths {
569 let relative_path;
598 let relative_path;
570 let relative_source;
599 let relative_source;
571 let (path, copy_source) = if let Some(relativize) =
600 let (path, copy_source) = if let Some(relativize) =
572 &self.relativize
601 &self.relativize
573 {
602 {
574 relative_path = relativize.relativize(&path);
603 relative_path = relativize.relativize(&path);
575 relative_source =
604 relative_source =
576 copy_source.as_ref().map(|s| relativize.relativize(s));
605 copy_source.as_ref().map(|s| relativize.relativize(s));
577 (&*relative_path, relative_source.as_deref())
606 (&*relative_path, relative_source.as_deref())
578 } else {
607 } else {
579 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
608 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
580 };
609 };
581 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
610 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
582 // in order to stream to stdout instead of allocating an
611 // in order to stream to stdout instead of allocating an
583 // itermediate `Vec<u8>`.
612 // itermediate `Vec<u8>`.
584 if !self.no_status {
613 if !self.no_status {
585 self.ui.write_stdout_labelled(status_prefix, label)?
614 self.ui.write_stdout_labelled(status_prefix, label)?
586 }
615 }
587 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
616 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
588 self.ui.write_stdout_labelled(
617 self.ui.write_stdout_labelled(
589 &format_bytes!(b"{}{}", path, linebreak),
618 &format_bytes!(b"{}{}", path, linebreak),
590 label,
619 label,
591 )?;
620 )?;
592 if let Some(source) = copy_source.filter(|_| !self.no_status) {
621 if let Some(source) = copy_source.filter(|_| !self.no_status) {
593 let label = "status.copied";
622 let label = "status.copied";
594 self.ui.write_stdout_labelled(
623 self.ui.write_stdout_labelled(
595 &format_bytes!(b" {}{}", source, linebreak),
624 &format_bytes!(b" {}{}", source, linebreak),
596 label,
625 label,
597 )?
626 )?
598 }
627 }
599 }
628 }
600 Ok(())
629 Ok(())
601 }
630 }
602 }
631 }
603
632
604 /// Outcome of the additional check for an ambiguous tracked file
633 /// Outcome of the additional check for an ambiguous tracked file
605 enum UnsureOutcome {
634 enum UnsureOutcome {
606 /// The file is actually clean
635 /// The file is actually clean
607 Clean,
636 Clean,
608 /// The file has been modified
637 /// The file has been modified
609 Modified,
638 Modified,
610 /// The file was deleted on disk (or became another type of fs entry)
639 /// The file was deleted on disk (or became another type of fs entry)
611 Deleted,
640 Deleted,
612 }
641 }
613
642
614 /// Check if a file is modified by comparing actual repo store and file system.
643 /// Check if a file is modified by comparing actual repo store and file system.
615 ///
644 ///
616 /// This meant to be used for those that the dirstate cannot resolve, due
645 /// This meant to be used for those that the dirstate cannot resolve, due
617 /// to time resolution limits.
646 /// to time resolution limits.
618 fn unsure_is_modified(
647 fn unsure_is_modified(
619 working_directory_vfs: hg::vfs::Vfs,
648 working_directory_vfs: hg::vfs::Vfs,
620 store_vfs: hg::vfs::Vfs,
649 store_vfs: hg::vfs::Vfs,
621 check_exec: bool,
650 check_exec: bool,
622 manifest: &Manifest,
651 manifest: &Manifest,
623 hg_path: &HgPath,
652 hg_path: &HgPath,
624 ) -> Result<UnsureOutcome, HgError> {
653 ) -> Result<UnsureOutcome, HgError> {
625 let vfs = working_directory_vfs;
654 let vfs = working_directory_vfs;
626 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
655 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
627 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
656 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
628 let is_symlink = fs_metadata.file_type().is_symlink();
657 let is_symlink = fs_metadata.file_type().is_symlink();
629
658
630 let entry = manifest
659 let entry = manifest
631 .find_by_path(hg_path)?
660 .find_by_path(hg_path)?
632 .expect("ambgious file not in p1");
661 .expect("ambgious file not in p1");
633
662
634 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
663 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
635 // dirstate
664 // dirstate
636 let fs_flags = if is_symlink {
665 let fs_flags = if is_symlink {
637 Some(b'l')
666 Some(b'l')
638 } else if check_exec && has_exec_bit(&fs_metadata) {
667 } else if check_exec && has_exec_bit(&fs_metadata) {
639 Some(b'x')
668 Some(b'x')
640 } else {
669 } else {
641 None
670 None
642 };
671 };
643
672
644 let entry_flags = if check_exec {
673 let entry_flags = if check_exec {
645 entry.flags
674 entry.flags
646 } else if entry.flags == Some(b'x') {
675 } else if entry.flags == Some(b'x') {
647 None
676 None
648 } else {
677 } else {
649 entry.flags
678 entry.flags
650 };
679 };
651
680
652 if entry_flags != fs_flags {
681 if entry_flags != fs_flags {
653 return Ok(UnsureOutcome::Modified);
682 return Ok(UnsureOutcome::Modified);
654 }
683 }
655 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
684 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
656 let fs_len = fs_metadata.len();
685 let fs_len = fs_metadata.len();
657 let file_node = entry.node_id()?;
686 let file_node = entry.node_id()?;
658 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
687 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
659 HgError::corrupted(format!(
688 HgError::corrupted(format!(
660 "filelog {:?} missing node {:?} from manifest",
689 "filelog {:?} missing node {:?} from manifest",
661 hg_path, file_node
690 hg_path, file_node
662 ))
691 ))
663 })?;
692 })?;
664 if filelog_entry.file_data_len_not_equal_to(fs_len) {
693 if filelog_entry.file_data_len_not_equal_to(fs_len) {
665 // No need to read file contents:
694 // No need to read file contents:
666 // it cannot be equal if it has a different length.
695 // it cannot be equal if it has a different length.
667 return Ok(UnsureOutcome::Modified);
696 return Ok(UnsureOutcome::Modified);
668 }
697 }
669
698
670 let p1_filelog_data = filelog_entry.data()?;
699 let p1_filelog_data = filelog_entry.data()?;
671 let p1_contents = p1_filelog_data.file_data()?;
700 let p1_contents = p1_filelog_data.file_data()?;
672 if p1_contents.len() as u64 != fs_len {
701 if p1_contents.len() as u64 != fs_len {
673 // No need to read file contents:
702 // No need to read file contents:
674 // it cannot be equal if it has a different length.
703 // it cannot be equal if it has a different length.
675 return Ok(UnsureOutcome::Modified);
704 return Ok(UnsureOutcome::Modified);
676 }
705 }
677
706
678 let fs_contents = if is_symlink {
707 let fs_contents = if is_symlink {
679 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
708 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
680 } else {
709 } else {
681 vfs.read(fs_path)?
710 vfs.read(fs_path)?
682 };
711 };
683
712
684 Ok(if p1_contents != &*fs_contents {
713 Ok(if p1_contents != &*fs_contents {
685 UnsureOutcome::Modified
714 UnsureOutcome::Modified
686 } else {
715 } else {
687 UnsureOutcome::Clean
716 UnsureOutcome::Clean
688 })
717 })
689 }
718 }
@@ -1,295 +1,308 b''
1 use crate::ui::utf8_to_local;
1 use crate::ui::utf8_to_local;
2 use crate::ui::UiError;
2 use crate::ui::UiError;
3 use crate::NoRepoInCwdError;
3 use crate::NoRepoInCwdError;
4 use format_bytes::format_bytes;
4 use format_bytes::format_bytes;
5 use hg::config::{ConfigError, ConfigParseError, ConfigValueParseError};
5 use hg::config::{ConfigError, ConfigParseError, ConfigValueParseError};
6 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
6 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
7 use hg::errors::HgError;
7 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::RevlogError;
10 use hg::revlog::RevlogError;
11 use hg::sparse::SparseConfigError;
11 use hg::sparse::SparseConfigError;
12 use hg::utils::files::get_bytes_from_path;
12 use hg::utils::files::get_bytes_from_path;
13 use hg::{DirstateError, DirstateMapError, StatusError};
13 use hg::utils::hg_path::HgPathError;
14 use hg::{DirstateError, DirstateMapError, PatternError, StatusError};
14 use std::convert::From;
15 use std::convert::From;
15
16
16 /// The kind of command error
17 /// The kind of command error
17 #[derive(Debug)]
18 #[derive(Debug)]
18 pub enum CommandError {
19 pub enum CommandError {
19 /// Exit with an error message and "standard" failure exit code.
20 /// Exit with an error message and "standard" failure exit code.
20 Abort {
21 Abort {
21 message: Vec<u8>,
22 message: Vec<u8>,
22 detailed_exit_code: exit_codes::ExitCode,
23 detailed_exit_code: exit_codes::ExitCode,
23 hint: Option<Vec<u8>>,
24 hint: Option<Vec<u8>>,
24 },
25 },
25
26
26 /// Exit with a failure exit code but no message.
27 /// Exit with a failure exit code but no message.
27 Unsuccessful,
28 Unsuccessful,
28
29
29 /// Encountered something (such as a CLI argument, repository layout, …)
30 /// Encountered something (such as a CLI argument, repository layout, …)
30 /// not supported by this version of `rhg`. Depending on configuration
31 /// not supported by this version of `rhg`. Depending on configuration
31 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
32 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
32 /// may or may not support this feature.
33 /// may or may not support this feature.
33 UnsupportedFeature { message: Vec<u8> },
34 UnsupportedFeature { message: Vec<u8> },
34 /// The fallback executable does not exist (or has some other problem if
35 /// The fallback executable does not exist (or has some other problem if
35 /// we end up being more precise about broken fallbacks).
36 /// we end up being more precise about broken fallbacks).
36 InvalidFallback { path: Vec<u8>, err: String },
37 InvalidFallback { path: Vec<u8>, err: String },
37 }
38 }
38
39
39 impl CommandError {
40 impl CommandError {
40 pub fn abort(message: impl AsRef<str>) -> Self {
41 pub fn abort(message: impl AsRef<str>) -> Self {
41 CommandError::abort_with_exit_code(message, exit_codes::ABORT)
42 CommandError::abort_with_exit_code(message, exit_codes::ABORT)
42 }
43 }
43
44
44 pub fn abort_with_exit_code(
45 pub fn abort_with_exit_code(
45 message: impl AsRef<str>,
46 message: impl AsRef<str>,
46 detailed_exit_code: exit_codes::ExitCode,
47 detailed_exit_code: exit_codes::ExitCode,
47 ) -> Self {
48 ) -> Self {
48 CommandError::Abort {
49 CommandError::Abort {
49 // TODO: bytes-based (instead of Unicode-based) formatting
50 // TODO: bytes-based (instead of Unicode-based) formatting
50 // of error messages to handle non-UTF-8 filenames etc:
51 // of error messages to handle non-UTF-8 filenames etc:
51 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
52 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
52 message: utf8_to_local(message.as_ref()).into(),
53 message: utf8_to_local(message.as_ref()).into(),
53 detailed_exit_code,
54 detailed_exit_code,
54 hint: None,
55 hint: None,
55 }
56 }
56 }
57 }
57
58
58 pub fn abort_with_exit_code_and_hint(
59 pub fn abort_with_exit_code_and_hint(
59 message: impl AsRef<str>,
60 message: impl AsRef<str>,
60 detailed_exit_code: exit_codes::ExitCode,
61 detailed_exit_code: exit_codes::ExitCode,
61 hint: Option<impl AsRef<str>>,
62 hint: Option<impl AsRef<str>>,
62 ) -> Self {
63 ) -> Self {
63 CommandError::Abort {
64 CommandError::Abort {
64 message: utf8_to_local(message.as_ref()).into(),
65 message: utf8_to_local(message.as_ref()).into(),
65 detailed_exit_code,
66 detailed_exit_code,
66 hint: hint.map(|h| utf8_to_local(h.as_ref()).into()),
67 hint: hint.map(|h| utf8_to_local(h.as_ref()).into()),
67 }
68 }
68 }
69 }
69
70
70 pub fn abort_with_exit_code_bytes(
71 pub fn abort_with_exit_code_bytes(
71 message: impl AsRef<[u8]>,
72 message: impl AsRef<[u8]>,
72 detailed_exit_code: exit_codes::ExitCode,
73 detailed_exit_code: exit_codes::ExitCode,
73 ) -> Self {
74 ) -> Self {
74 // TODO: use this everywhere it makes sense instead of the string
75 // TODO: use this everywhere it makes sense instead of the string
75 // version.
76 // version.
76 CommandError::Abort {
77 CommandError::Abort {
77 message: message.as_ref().into(),
78 message: message.as_ref().into(),
78 detailed_exit_code,
79 detailed_exit_code,
79 hint: None,
80 hint: None,
80 }
81 }
81 }
82 }
82
83
83 pub fn unsupported(message: impl AsRef<str>) -> Self {
84 pub fn unsupported(message: impl AsRef<str>) -> Self {
84 CommandError::UnsupportedFeature {
85 CommandError::UnsupportedFeature {
85 message: utf8_to_local(message.as_ref()).into(),
86 message: utf8_to_local(message.as_ref()).into(),
86 }
87 }
87 }
88 }
88 }
89 }
89
90
90 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
91 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
91 /// but not supported yet by `rhg`.
92 /// but not supported yet by `rhg`.
92 impl From<clap::Error> for CommandError {
93 impl From<clap::Error> for CommandError {
93 fn from(error: clap::Error) -> Self {
94 fn from(error: clap::Error) -> Self {
94 CommandError::unsupported(error.to_string())
95 CommandError::unsupported(error.to_string())
95 }
96 }
96 }
97 }
97
98
98 impl From<HgError> for CommandError {
99 impl From<HgError> for CommandError {
99 fn from(error: HgError) -> Self {
100 fn from(error: HgError) -> Self {
100 match error {
101 match error {
101 HgError::UnsupportedFeature(message) => {
102 HgError::UnsupportedFeature(message) => {
102 CommandError::unsupported(message)
103 CommandError::unsupported(message)
103 }
104 }
104 HgError::CensoredNodeError => {
105 HgError::CensoredNodeError => {
105 CommandError::unsupported("Encountered a censored node")
106 CommandError::unsupported("Encountered a censored node")
106 }
107 }
107 HgError::Abort {
108 HgError::Abort {
108 message,
109 message,
109 detailed_exit_code,
110 detailed_exit_code,
110 hint,
111 hint,
111 } => CommandError::abort_with_exit_code_and_hint(
112 } => CommandError::abort_with_exit_code_and_hint(
112 message,
113 message,
113 detailed_exit_code,
114 detailed_exit_code,
114 hint,
115 hint,
115 ),
116 ),
116 _ => CommandError::abort(error.to_string()),
117 _ => CommandError::abort(error.to_string()),
117 }
118 }
118 }
119 }
119 }
120 }
120
121
121 impl From<ConfigValueParseError> for CommandError {
122 impl From<ConfigValueParseError> for CommandError {
122 fn from(error: ConfigValueParseError) -> Self {
123 fn from(error: ConfigValueParseError) -> Self {
123 CommandError::abort_with_exit_code(
124 CommandError::abort_with_exit_code(
124 error.to_string(),
125 error.to_string(),
125 exit_codes::CONFIG_ERROR_ABORT,
126 exit_codes::CONFIG_ERROR_ABORT,
126 )
127 )
127 }
128 }
128 }
129 }
129
130
130 impl From<UiError> for CommandError {
131 impl From<UiError> for CommandError {
131 fn from(_error: UiError) -> Self {
132 fn from(_error: UiError) -> Self {
132 // If we already failed writing to stdout or stderr,
133 // If we already failed writing to stdout or stderr,
133 // writing an error message to stderr about it would be likely to fail
134 // writing an error message to stderr about it would be likely to fail
134 // too.
135 // too.
135 CommandError::abort("")
136 CommandError::abort("")
136 }
137 }
137 }
138 }
138
139
139 impl From<RepoError> for CommandError {
140 impl From<RepoError> for CommandError {
140 fn from(error: RepoError) -> Self {
141 fn from(error: RepoError) -> Self {
141 match error {
142 match error {
142 RepoError::NotFound { at } => {
143 RepoError::NotFound { at } => {
143 CommandError::abort_with_exit_code_bytes(
144 CommandError::abort_with_exit_code_bytes(
144 format_bytes!(
145 format_bytes!(
145 b"abort: repository {} not found",
146 b"abort: repository {} not found",
146 get_bytes_from_path(at)
147 get_bytes_from_path(at)
147 ),
148 ),
148 exit_codes::ABORT,
149 exit_codes::ABORT,
149 )
150 )
150 }
151 }
151 RepoError::ConfigParseError(error) => error.into(),
152 RepoError::ConfigParseError(error) => error.into(),
152 RepoError::Other(error) => error.into(),
153 RepoError::Other(error) => error.into(),
153 }
154 }
154 }
155 }
155 }
156 }
156
157
157 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
158 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
158 fn from(error: &'a NoRepoInCwdError) -> Self {
159 fn from(error: &'a NoRepoInCwdError) -> Self {
159 let NoRepoInCwdError { cwd } = error;
160 let NoRepoInCwdError { cwd } = error;
160 CommandError::abort_with_exit_code_bytes(
161 CommandError::abort_with_exit_code_bytes(
161 format_bytes!(
162 format_bytes!(
162 b"abort: no repository found in '{}' (.hg not found)!",
163 b"abort: no repository found in '{}' (.hg not found)!",
163 get_bytes_from_path(cwd)
164 get_bytes_from_path(cwd)
164 ),
165 ),
165 exit_codes::ABORT,
166 exit_codes::ABORT,
166 )
167 )
167 }
168 }
168 }
169 }
169
170
170 impl From<ConfigError> for CommandError {
171 impl From<ConfigError> for CommandError {
171 fn from(error: ConfigError) -> Self {
172 fn from(error: ConfigError) -> Self {
172 match error {
173 match error {
173 ConfigError::Parse(error) => error.into(),
174 ConfigError::Parse(error) => error.into(),
174 ConfigError::Other(error) => error.into(),
175 ConfigError::Other(error) => error.into(),
175 }
176 }
176 }
177 }
177 }
178 }
178
179
179 impl From<ConfigParseError> for CommandError {
180 impl From<ConfigParseError> for CommandError {
180 fn from(error: ConfigParseError) -> Self {
181 fn from(error: ConfigParseError) -> Self {
181 let ConfigParseError {
182 let ConfigParseError {
182 origin,
183 origin,
183 line,
184 line,
184 message,
185 message,
185 } = error;
186 } = error;
186 let line_message = if let Some(line_number) = line {
187 let line_message = if let Some(line_number) = line {
187 format_bytes!(b":{}", line_number.to_string().into_bytes())
188 format_bytes!(b":{}", line_number.to_string().into_bytes())
188 } else {
189 } else {
189 Vec::new()
190 Vec::new()
190 };
191 };
191 CommandError::abort_with_exit_code_bytes(
192 CommandError::abort_with_exit_code_bytes(
192 format_bytes!(
193 format_bytes!(
193 b"config error at {}{}: {}",
194 b"config error at {}{}: {}",
194 origin,
195 origin,
195 line_message,
196 line_message,
196 message
197 message
197 ),
198 ),
198 exit_codes::CONFIG_ERROR_ABORT,
199 exit_codes::CONFIG_ERROR_ABORT,
199 )
200 )
200 }
201 }
201 }
202 }
202
203
203 impl From<(RevlogError, &str)> for CommandError {
204 impl From<(RevlogError, &str)> for CommandError {
204 fn from((err, rev): (RevlogError, &str)) -> CommandError {
205 fn from((err, rev): (RevlogError, &str)) -> CommandError {
205 match err {
206 match err {
206 RevlogError::WDirUnsupported => CommandError::abort(
207 RevlogError::WDirUnsupported => CommandError::abort(
207 "abort: working directory revision cannot be specified",
208 "abort: working directory revision cannot be specified",
208 ),
209 ),
209 RevlogError::InvalidRevision => CommandError::abort(format!(
210 RevlogError::InvalidRevision => CommandError::abort(format!(
210 "abort: invalid revision identifier: {}",
211 "abort: invalid revision identifier: {}",
211 rev
212 rev
212 )),
213 )),
213 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
214 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
214 "abort: ambiguous revision identifier: {}",
215 "abort: ambiguous revision identifier: {}",
215 rev
216 rev
216 )),
217 )),
217 RevlogError::Other(error) => error.into(),
218 RevlogError::Other(error) => error.into(),
218 }
219 }
219 }
220 }
220 }
221 }
221
222
222 impl From<StatusError> for CommandError {
223 impl From<StatusError> for CommandError {
223 fn from(error: StatusError) -> Self {
224 fn from(error: StatusError) -> Self {
224 match error {
225 match error {
225 StatusError::Pattern(_) => {
226 StatusError::Pattern(_) => {
226 CommandError::unsupported(format!("{}", error))
227 CommandError::unsupported(format!("{}", error))
227 }
228 }
228 _ => CommandError::abort(format!("{}", error)),
229 _ => CommandError::abort(format!("{}", error)),
229 }
230 }
230 }
231 }
231 }
232 }
232
233
234 impl From<HgPathError> for CommandError {
235 fn from(error: HgPathError) -> Self {
236 CommandError::unsupported(format!("{}", error))
237 }
238 }
239
240 impl From<PatternError> for CommandError {
241 fn from(error: PatternError) -> Self {
242 CommandError::unsupported(format!("{}", error))
243 }
244 }
245
233 impl From<DirstateMapError> for CommandError {
246 impl From<DirstateMapError> for CommandError {
234 fn from(error: DirstateMapError) -> Self {
247 fn from(error: DirstateMapError) -> Self {
235 CommandError::abort(format!("{}", error))
248 CommandError::abort(format!("{}", error))
236 }
249 }
237 }
250 }
238
251
239 impl From<DirstateError> for CommandError {
252 impl From<DirstateError> for CommandError {
240 fn from(error: DirstateError) -> Self {
253 fn from(error: DirstateError) -> Self {
241 match error {
254 match error {
242 DirstateError::Common(error) => error.into(),
255 DirstateError::Common(error) => error.into(),
243 DirstateError::Map(error) => error.into(),
256 DirstateError::Map(error) => error.into(),
244 }
257 }
245 }
258 }
246 }
259 }
247
260
248 impl From<DirstateV2ParseError> for CommandError {
261 impl From<DirstateV2ParseError> for CommandError {
249 fn from(error: DirstateV2ParseError) -> Self {
262 fn from(error: DirstateV2ParseError) -> Self {
250 HgError::from(error).into()
263 HgError::from(error).into()
251 }
264 }
252 }
265 }
253
266
254 impl From<SparseConfigError> for CommandError {
267 impl From<SparseConfigError> for CommandError {
255 fn from(e: SparseConfigError) -> Self {
268 fn from(e: SparseConfigError) -> Self {
256 match e {
269 match e {
257 SparseConfigError::IncludesAfterExcludes { context } => {
270 SparseConfigError::IncludesAfterExcludes { context } => {
258 Self::abort_with_exit_code_bytes(
271 Self::abort_with_exit_code_bytes(
259 format_bytes!(
272 format_bytes!(
260 b"{} config cannot have includes after excludes",
273 b"{} config cannot have includes after excludes",
261 context
274 context
262 ),
275 ),
263 exit_codes::CONFIG_PARSE_ERROR_ABORT,
276 exit_codes::CONFIG_PARSE_ERROR_ABORT,
264 )
277 )
265 }
278 }
266 SparseConfigError::EntryOutsideSection { context, line } => {
279 SparseConfigError::EntryOutsideSection { context, line } => {
267 Self::abort_with_exit_code_bytes(
280 Self::abort_with_exit_code_bytes(
268 format_bytes!(
281 format_bytes!(
269 b"{} config entry outside of section: {}",
282 b"{} config entry outside of section: {}",
270 context,
283 context,
271 &line,
284 &line,
272 ),
285 ),
273 exit_codes::CONFIG_PARSE_ERROR_ABORT,
286 exit_codes::CONFIG_PARSE_ERROR_ABORT,
274 )
287 )
275 }
288 }
276 SparseConfigError::InvalidNarrowPrefix(prefix) => {
289 SparseConfigError::InvalidNarrowPrefix(prefix) => {
277 Self::abort_with_exit_code_bytes(
290 Self::abort_with_exit_code_bytes(
278 format_bytes!(
291 format_bytes!(
279 b"invalid prefix on narrow pattern: {}",
292 b"invalid prefix on narrow pattern: {}",
280 &prefix
293 &prefix
281 ),
294 ),
282 exit_codes::ABORT,
295 exit_codes::ABORT,
283 )
296 )
284 }
297 }
285 SparseConfigError::IncludesInNarrow => Self::abort(
298 SparseConfigError::IncludesInNarrow => Self::abort(
286 "including other spec files using '%include' \
299 "including other spec files using '%include' \
287 is not supported in narrowspec",
300 is not supported in narrowspec",
288 ),
301 ),
289 SparseConfigError::HgError(e) => Self::from(e),
302 SparseConfigError::HgError(e) => Self::from(e),
290 SparseConfigError::PatternError(e) => {
303 SparseConfigError::PatternError(e) => {
291 Self::unsupported(format!("{}", e))
304 Self::unsupported(format!("{}", e))
292 }
305 }
293 }
306 }
294 }
307 }
295 }
308 }
General Comments 0
You need to be logged in to leave comments. Login now