##// END OF EJS Templates
rust-status: fix ignore and include not composing (issue6514)...
Raphaël Gomès -
r48102:c8f62920 stable
parent child Browse files
Show More
@@ -1,913 +1,915 b''
1 // status.rs
1 // status.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 //! Rust implementation of dirstate.status (dirstate.py).
8 //! Rust implementation of dirstate.status (dirstate.py).
9 //! It is currently missing a lot of functionality compared to the Python one
9 //! It is currently missing a lot of functionality compared to the Python one
10 //! and will only be triggered in narrow cases.
10 //! and will only be triggered in narrow cases.
11
11
12 use crate::utils::path_auditor::PathAuditor;
12 use crate::utils::path_auditor::PathAuditor;
13 use crate::{
13 use crate::{
14 dirstate::SIZE_FROM_OTHER_PARENT,
14 dirstate::SIZE_FROM_OTHER_PARENT,
15 filepatterns::PatternFileWarning,
15 filepatterns::PatternFileWarning,
16 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
16 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
17 utils::{
17 utils::{
18 files::{find_dirs, HgMetadata},
18 files::{find_dirs, HgMetadata},
19 hg_path::{
19 hg_path::{
20 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
20 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
21 HgPathError,
21 HgPathError,
22 },
22 },
23 },
23 },
24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
25 PatternError,
25 PatternError,
26 };
26 };
27 use lazy_static::lazy_static;
27 use lazy_static::lazy_static;
28 use micro_timer::timed;
28 use micro_timer::timed;
29 use rayon::prelude::*;
29 use rayon::prelude::*;
30 use std::{
30 use std::{
31 borrow::Cow,
31 borrow::Cow,
32 collections::HashSet,
32 collections::HashSet,
33 fmt,
33 fmt,
34 fs::{read_dir, DirEntry},
34 fs::{read_dir, DirEntry},
35 io::ErrorKind,
35 io::ErrorKind,
36 ops::Deref,
36 ops::Deref,
37 path::{Path, PathBuf},
37 path::{Path, PathBuf},
38 };
38 };
39
39
40 /// Wrong type of file from a `BadMatch`
40 /// Wrong type of file from a `BadMatch`
41 /// Note: a lot of those don't exist on all platforms.
41 /// Note: a lot of those don't exist on all platforms.
42 #[derive(Debug, Copy, Clone)]
42 #[derive(Debug, Copy, Clone)]
43 pub enum BadType {
43 pub enum BadType {
44 CharacterDevice,
44 CharacterDevice,
45 BlockDevice,
45 BlockDevice,
46 FIFO,
46 FIFO,
47 Socket,
47 Socket,
48 Directory,
48 Directory,
49 Unknown,
49 Unknown,
50 }
50 }
51
51
52 impl fmt::Display for BadType {
52 impl fmt::Display for BadType {
53 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
53 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54 f.write_str(match self {
54 f.write_str(match self {
55 BadType::CharacterDevice => "character device",
55 BadType::CharacterDevice => "character device",
56 BadType::BlockDevice => "block device",
56 BadType::BlockDevice => "block device",
57 BadType::FIFO => "fifo",
57 BadType::FIFO => "fifo",
58 BadType::Socket => "socket",
58 BadType::Socket => "socket",
59 BadType::Directory => "directory",
59 BadType::Directory => "directory",
60 BadType::Unknown => "unknown",
60 BadType::Unknown => "unknown",
61 })
61 })
62 }
62 }
63 }
63 }
64
64
65 /// Was explicitly matched but cannot be found/accessed
65 /// Was explicitly matched but cannot be found/accessed
66 #[derive(Debug, Copy, Clone)]
66 #[derive(Debug, Copy, Clone)]
67 pub enum BadMatch {
67 pub enum BadMatch {
68 OsError(i32),
68 OsError(i32),
69 BadType(BadType),
69 BadType(BadType),
70 }
70 }
71
71
72 /// Enum used to dispatch new status entries into the right collections.
72 /// Enum used to dispatch new status entries into the right collections.
73 /// Is similar to `crate::EntryState`, but represents the transient state of
73 /// Is similar to `crate::EntryState`, but represents the transient state of
74 /// entries during the lifetime of a command.
74 /// entries during the lifetime of a command.
75 #[derive(Debug, Copy, Clone)]
75 #[derive(Debug, Copy, Clone)]
76 pub enum Dispatch {
76 pub enum Dispatch {
77 Unsure,
77 Unsure,
78 Modified,
78 Modified,
79 Added,
79 Added,
80 Removed,
80 Removed,
81 Deleted,
81 Deleted,
82 Clean,
82 Clean,
83 Unknown,
83 Unknown,
84 Ignored,
84 Ignored,
85 /// Empty dispatch, the file is not worth listing
85 /// Empty dispatch, the file is not worth listing
86 None,
86 None,
87 /// Was explicitly matched but cannot be found/accessed
87 /// Was explicitly matched but cannot be found/accessed
88 Bad(BadMatch),
88 Bad(BadMatch),
89 Directory {
89 Directory {
90 /// True if the directory used to be a file in the dmap so we can say
90 /// True if the directory used to be a file in the dmap so we can say
91 /// that it's been removed.
91 /// that it's been removed.
92 was_file: bool,
92 was_file: bool,
93 },
93 },
94 }
94 }
95
95
96 type IoResult<T> = std::io::Result<T>;
96 type IoResult<T> = std::io::Result<T>;
97
97
98 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
98 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
99 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
99 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
100 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
100 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
101
101
102 /// We have a good mix of owned (from directory traversal) and borrowed (from
102 /// We have a good mix of owned (from directory traversal) and borrowed (from
103 /// the dirstate/explicit) paths, this comes up a lot.
103 /// the dirstate/explicit) paths, this comes up a lot.
104 pub type HgPathCow<'a> = Cow<'a, HgPath>;
104 pub type HgPathCow<'a> = Cow<'a, HgPath>;
105
105
106 /// A path with its computed ``Dispatch`` information
106 /// A path with its computed ``Dispatch`` information
107 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch);
107 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch);
108
108
109 /// The conversion from `HgPath` to a real fs path failed.
109 /// The conversion from `HgPath` to a real fs path failed.
110 /// `22` is the error code for "Invalid argument"
110 /// `22` is the error code for "Invalid argument"
111 const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22));
111 const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22));
112
112
113 /// Dates and times that are outside the 31-bit signed range are compared
113 /// Dates and times that are outside the 31-bit signed range are compared
114 /// modulo 2^31. This should prevent hg from behaving badly with very large
114 /// modulo 2^31. This should prevent hg from behaving badly with very large
115 /// files or corrupt dates while still having a high probability of detecting
115 /// files or corrupt dates while still having a high probability of detecting
116 /// changes. (issue2608)
116 /// changes. (issue2608)
117 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
117 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
118 /// is not defined for `i32`, and there is no `As` trait. This forces the
118 /// is not defined for `i32`, and there is no `As` trait. This forces the
119 /// caller to cast `b` as `i32`.
119 /// caller to cast `b` as `i32`.
120 fn mod_compare(a: i32, b: i32) -> bool {
120 fn mod_compare(a: i32, b: i32) -> bool {
121 a & i32::max_value() != b & i32::max_value()
121 a & i32::max_value() != b & i32::max_value()
122 }
122 }
123
123
124 /// Return a sorted list containing information about the entries
124 /// Return a sorted list containing information about the entries
125 /// in the directory.
125 /// in the directory.
126 ///
126 ///
127 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
127 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
128 fn list_directory(
128 fn list_directory(
129 path: impl AsRef<Path>,
129 path: impl AsRef<Path>,
130 skip_dot_hg: bool,
130 skip_dot_hg: bool,
131 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
131 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
132 let mut results = vec![];
132 let mut results = vec![];
133 let entries = read_dir(path.as_ref())?;
133 let entries = read_dir(path.as_ref())?;
134
134
135 for entry in entries {
135 for entry in entries {
136 let entry = entry?;
136 let entry = entry?;
137 let filename = os_string_to_hg_path_buf(entry.file_name())?;
137 let filename = os_string_to_hg_path_buf(entry.file_name())?;
138 let file_type = entry.file_type()?;
138 let file_type = entry.file_type()?;
139 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
139 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
140 return Ok(vec![]);
140 return Ok(vec![]);
141 } else {
141 } else {
142 results.push((filename, entry))
142 results.push((filename, entry))
143 }
143 }
144 }
144 }
145
145
146 results.sort_unstable_by_key(|e| e.0.clone());
146 results.sort_unstable_by_key(|e| e.0.clone());
147 Ok(results)
147 Ok(results)
148 }
148 }
149
149
150 /// The file corresponding to the dirstate entry was found on the filesystem.
150 /// The file corresponding to the dirstate entry was found on the filesystem.
151 fn dispatch_found(
151 fn dispatch_found(
152 filename: impl AsRef<HgPath>,
152 filename: impl AsRef<HgPath>,
153 entry: DirstateEntry,
153 entry: DirstateEntry,
154 metadata: HgMetadata,
154 metadata: HgMetadata,
155 copy_map: &CopyMap,
155 copy_map: &CopyMap,
156 options: StatusOptions,
156 options: StatusOptions,
157 ) -> Dispatch {
157 ) -> Dispatch {
158 let DirstateEntry {
158 let DirstateEntry {
159 state,
159 state,
160 mode,
160 mode,
161 mtime,
161 mtime,
162 size,
162 size,
163 } = entry;
163 } = entry;
164
164
165 let HgMetadata {
165 let HgMetadata {
166 st_mode,
166 st_mode,
167 st_size,
167 st_size,
168 st_mtime,
168 st_mtime,
169 ..
169 ..
170 } = metadata;
170 } = metadata;
171
171
172 match state {
172 match state {
173 EntryState::Normal => {
173 EntryState::Normal => {
174 let size_changed = mod_compare(size, st_size as i32);
174 let size_changed = mod_compare(size, st_size as i32);
175 let mode_changed =
175 let mode_changed =
176 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
176 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
177 let metadata_changed = size >= 0 && (size_changed || mode_changed);
177 let metadata_changed = size >= 0 && (size_changed || mode_changed);
178 let other_parent = size == SIZE_FROM_OTHER_PARENT;
178 let other_parent = size == SIZE_FROM_OTHER_PARENT;
179
179
180 if metadata_changed
180 if metadata_changed
181 || other_parent
181 || other_parent
182 || copy_map.contains_key(filename.as_ref())
182 || copy_map.contains_key(filename.as_ref())
183 {
183 {
184 if metadata.is_symlink() && size_changed {
184 if metadata.is_symlink() && size_changed {
185 // issue6456: Size returned may be longer due to encryption
185 // issue6456: Size returned may be longer due to encryption
186 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
186 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
187 Dispatch::Unsure
187 Dispatch::Unsure
188 } else {
188 } else {
189 Dispatch::Modified
189 Dispatch::Modified
190 }
190 }
191 } else if mod_compare(mtime, st_mtime as i32)
191 } else if mod_compare(mtime, st_mtime as i32)
192 || st_mtime == options.last_normal_time
192 || st_mtime == options.last_normal_time
193 {
193 {
194 // the file may have just been marked as normal and
194 // the file may have just been marked as normal and
195 // it may have changed in the same second without
195 // it may have changed in the same second without
196 // changing its size. This can happen if we quickly
196 // changing its size. This can happen if we quickly
197 // do multiple commits. Force lookup, so we don't
197 // do multiple commits. Force lookup, so we don't
198 // miss such a racy file change.
198 // miss such a racy file change.
199 Dispatch::Unsure
199 Dispatch::Unsure
200 } else if options.list_clean {
200 } else if options.list_clean {
201 Dispatch::Clean
201 Dispatch::Clean
202 } else {
202 } else {
203 Dispatch::None
203 Dispatch::None
204 }
204 }
205 }
205 }
206 EntryState::Merged => Dispatch::Modified,
206 EntryState::Merged => Dispatch::Modified,
207 EntryState::Added => Dispatch::Added,
207 EntryState::Added => Dispatch::Added,
208 EntryState::Removed => Dispatch::Removed,
208 EntryState::Removed => Dispatch::Removed,
209 EntryState::Unknown => Dispatch::Unknown,
209 EntryState::Unknown => Dispatch::Unknown,
210 }
210 }
211 }
211 }
212
212
213 /// The file corresponding to this Dirstate entry is missing.
213 /// The file corresponding to this Dirstate entry is missing.
214 fn dispatch_missing(state: EntryState) -> Dispatch {
214 fn dispatch_missing(state: EntryState) -> Dispatch {
215 match state {
215 match state {
216 // File was removed from the filesystem during commands
216 // File was removed from the filesystem during commands
217 EntryState::Normal | EntryState::Merged | EntryState::Added => {
217 EntryState::Normal | EntryState::Merged | EntryState::Added => {
218 Dispatch::Deleted
218 Dispatch::Deleted
219 }
219 }
220 // File was removed, everything is normal
220 // File was removed, everything is normal
221 EntryState::Removed => Dispatch::Removed,
221 EntryState::Removed => Dispatch::Removed,
222 // File is unknown to Mercurial, everything is normal
222 // File is unknown to Mercurial, everything is normal
223 EntryState::Unknown => Dispatch::Unknown,
223 EntryState::Unknown => Dispatch::Unknown,
224 }
224 }
225 }
225 }
226
226
227 fn dispatch_os_error(e: &std::io::Error) -> Dispatch {
227 fn dispatch_os_error(e: &std::io::Error) -> Dispatch {
228 Dispatch::Bad(BadMatch::OsError(
228 Dispatch::Bad(BadMatch::OsError(
229 e.raw_os_error().expect("expected real OS error"),
229 e.raw_os_error().expect("expected real OS error"),
230 ))
230 ))
231 }
231 }
232
232
233 lazy_static! {
233 lazy_static! {
234 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
234 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
235 let mut h = HashSet::new();
235 let mut h = HashSet::new();
236 h.insert(HgPath::new(b""));
236 h.insert(HgPath::new(b""));
237 h
237 h
238 };
238 };
239 }
239 }
240
240
241 #[derive(Debug, Copy, Clone)]
241 #[derive(Debug, Copy, Clone)]
242 pub struct StatusOptions {
242 pub struct StatusOptions {
243 /// Remember the most recent modification timeslot for status, to make
243 /// Remember the most recent modification timeslot for status, to make
244 /// sure we won't miss future size-preserving file content modifications
244 /// sure we won't miss future size-preserving file content modifications
245 /// that happen within the same timeslot.
245 /// that happen within the same timeslot.
246 pub last_normal_time: i64,
246 pub last_normal_time: i64,
247 /// Whether we are on a filesystem with UNIX-like exec flags
247 /// Whether we are on a filesystem with UNIX-like exec flags
248 pub check_exec: bool,
248 pub check_exec: bool,
249 pub list_clean: bool,
249 pub list_clean: bool,
250 pub list_unknown: bool,
250 pub list_unknown: bool,
251 pub list_ignored: bool,
251 pub list_ignored: bool,
252 /// Whether to collect traversed dirs for applying a callback later.
252 /// Whether to collect traversed dirs for applying a callback later.
253 /// Used by `hg purge` for example.
253 /// Used by `hg purge` for example.
254 pub collect_traversed_dirs: bool,
254 pub collect_traversed_dirs: bool,
255 }
255 }
256
256
257 #[derive(Debug)]
257 #[derive(Debug)]
258 pub struct DirstateStatus<'a> {
258 pub struct DirstateStatus<'a> {
259 pub modified: Vec<HgPathCow<'a>>,
259 pub modified: Vec<HgPathCow<'a>>,
260 pub added: Vec<HgPathCow<'a>>,
260 pub added: Vec<HgPathCow<'a>>,
261 pub removed: Vec<HgPathCow<'a>>,
261 pub removed: Vec<HgPathCow<'a>>,
262 pub deleted: Vec<HgPathCow<'a>>,
262 pub deleted: Vec<HgPathCow<'a>>,
263 pub clean: Vec<HgPathCow<'a>>,
263 pub clean: Vec<HgPathCow<'a>>,
264 pub ignored: Vec<HgPathCow<'a>>,
264 pub ignored: Vec<HgPathCow<'a>>,
265 pub unknown: Vec<HgPathCow<'a>>,
265 pub unknown: Vec<HgPathCow<'a>>,
266 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
266 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
267 /// Only filled if `collect_traversed_dirs` is `true`
267 /// Only filled if `collect_traversed_dirs` is `true`
268 pub traversed: Vec<HgPathBuf>,
268 pub traversed: Vec<HgPathBuf>,
269 }
269 }
270
270
271 #[derive(Debug, derive_more::From)]
271 #[derive(Debug, derive_more::From)]
272 pub enum StatusError {
272 pub enum StatusError {
273 /// Generic IO error
273 /// Generic IO error
274 IO(std::io::Error),
274 IO(std::io::Error),
275 /// An invalid path that cannot be represented in Mercurial was found
275 /// An invalid path that cannot be represented in Mercurial was found
276 Path(HgPathError),
276 Path(HgPathError),
277 /// An invalid "ignore" pattern was found
277 /// An invalid "ignore" pattern was found
278 Pattern(PatternError),
278 Pattern(PatternError),
279 }
279 }
280
280
281 pub type StatusResult<T> = Result<T, StatusError>;
281 pub type StatusResult<T> = Result<T, StatusError>;
282
282
283 impl fmt::Display for StatusError {
283 impl fmt::Display for StatusError {
284 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
284 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
285 match self {
285 match self {
286 StatusError::IO(error) => error.fmt(f),
286 StatusError::IO(error) => error.fmt(f),
287 StatusError::Path(error) => error.fmt(f),
287 StatusError::Path(error) => error.fmt(f),
288 StatusError::Pattern(error) => error.fmt(f),
288 StatusError::Pattern(error) => error.fmt(f),
289 }
289 }
290 }
290 }
291 }
291 }
292
292
293 /// Gives information about which files are changed in the working directory
293 /// Gives information about which files are changed in the working directory
294 /// and how, compared to the revision we're based on
294 /// and how, compared to the revision we're based on
295 pub struct Status<'a, M: Matcher + Sync> {
295 pub struct Status<'a, M: Matcher + Sync> {
296 dmap: &'a DirstateMap,
296 dmap: &'a DirstateMap,
297 pub(crate) matcher: &'a M,
297 pub(crate) matcher: &'a M,
298 root_dir: PathBuf,
298 root_dir: PathBuf,
299 pub(crate) options: StatusOptions,
299 pub(crate) options: StatusOptions,
300 ignore_fn: IgnoreFnType<'a>,
300 ignore_fn: IgnoreFnType<'a>,
301 }
301 }
302
302
303 impl<'a, M> Status<'a, M>
303 impl<'a, M> Status<'a, M>
304 where
304 where
305 M: Matcher + Sync,
305 M: Matcher + Sync,
306 {
306 {
307 pub fn new(
307 pub fn new(
308 dmap: &'a DirstateMap,
308 dmap: &'a DirstateMap,
309 matcher: &'a M,
309 matcher: &'a M,
310 root_dir: PathBuf,
310 root_dir: PathBuf,
311 ignore_files: Vec<PathBuf>,
311 ignore_files: Vec<PathBuf>,
312 options: StatusOptions,
312 options: StatusOptions,
313 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
313 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
314 // Needs to outlive `dir_ignore_fn` since it's captured.
314 // Needs to outlive `dir_ignore_fn` since it's captured.
315
315
316 let (ignore_fn, warnings): (IgnoreFnType, _) =
316 let (ignore_fn, warnings): (IgnoreFnType, _) =
317 if options.list_ignored || options.list_unknown {
317 if options.list_ignored || options.list_unknown {
318 get_ignore_function(ignore_files, &root_dir)?
318 get_ignore_function(ignore_files, &root_dir)?
319 } else {
319 } else {
320 (Box::new(|&_| true), vec![])
320 (Box::new(|&_| true), vec![])
321 };
321 };
322
322
323 Ok((
323 Ok((
324 Self {
324 Self {
325 dmap,
325 dmap,
326 matcher,
326 matcher,
327 root_dir,
327 root_dir,
328 options,
328 options,
329 ignore_fn,
329 ignore_fn,
330 },
330 },
331 warnings,
331 warnings,
332 ))
332 ))
333 }
333 }
334
334
335 /// Is the path ignored?
335 /// Is the path ignored?
336 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
336 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
337 (self.ignore_fn)(path.as_ref())
337 (self.ignore_fn)(path.as_ref())
338 }
338 }
339
339
340 /// Is the path or one of its ancestors ignored?
340 /// Is the path or one of its ancestors ignored?
341 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
341 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
342 // Only involve ignore mechanism if we're listing unknowns or ignored.
342 // Only involve ignore mechanism if we're listing unknowns or ignored.
343 if self.options.list_ignored || self.options.list_unknown {
343 if self.options.list_ignored || self.options.list_unknown {
344 if self.is_ignored(&dir) {
344 if self.is_ignored(&dir) {
345 true
345 true
346 } else {
346 } else {
347 for p in find_dirs(dir.as_ref()) {
347 for p in find_dirs(dir.as_ref()) {
348 if self.is_ignored(p) {
348 if self.is_ignored(p) {
349 return true;
349 return true;
350 }
350 }
351 }
351 }
352 false
352 false
353 }
353 }
354 } else {
354 } else {
355 true
355 true
356 }
356 }
357 }
357 }
358
358
359 /// Get stat data about the files explicitly specified by the matcher.
359 /// Get stat data about the files explicitly specified by the matcher.
360 /// Returns a tuple of the directories that need to be traversed and the
360 /// Returns a tuple of the directories that need to be traversed and the
361 /// files with their corresponding `Dispatch`.
361 /// files with their corresponding `Dispatch`.
362 /// TODO subrepos
362 /// TODO subrepos
363 #[timed]
363 #[timed]
364 pub fn walk_explicit(
364 pub fn walk_explicit(
365 &self,
365 &self,
366 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
366 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
367 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) {
367 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) {
368 self.matcher
368 self.matcher
369 .file_set()
369 .file_set()
370 .unwrap_or(&DEFAULT_WORK)
370 .unwrap_or(&DEFAULT_WORK)
371 .par_iter()
371 .par_iter()
372 .flat_map(|&filename| -> Option<_> {
372 .flat_map(|&filename| -> Option<_> {
373 // TODO normalization
373 // TODO normalization
374 let normalized = filename;
374 let normalized = filename;
375
375
376 let buf = match hg_path_to_path_buf(normalized) {
376 let buf = match hg_path_to_path_buf(normalized) {
377 Ok(x) => x,
377 Ok(x) => x,
378 Err(_) => {
378 Err(_) => {
379 return Some((
379 return Some((
380 Cow::Borrowed(normalized),
380 Cow::Borrowed(normalized),
381 INVALID_PATH_DISPATCH,
381 INVALID_PATH_DISPATCH,
382 ))
382 ))
383 }
383 }
384 };
384 };
385 let target = self.root_dir.join(buf);
385 let target = self.root_dir.join(buf);
386 let st = target.symlink_metadata();
386 let st = target.symlink_metadata();
387 let in_dmap = self.dmap.get(normalized);
387 let in_dmap = self.dmap.get(normalized);
388 match st {
388 match st {
389 Ok(meta) => {
389 Ok(meta) => {
390 let file_type = meta.file_type();
390 let file_type = meta.file_type();
391 return if file_type.is_file() || file_type.is_symlink()
391 return if file_type.is_file() || file_type.is_symlink()
392 {
392 {
393 if let Some(entry) = in_dmap {
393 if let Some(entry) = in_dmap {
394 return Some((
394 return Some((
395 Cow::Borrowed(normalized),
395 Cow::Borrowed(normalized),
396 dispatch_found(
396 dispatch_found(
397 &normalized,
397 &normalized,
398 *entry,
398 *entry,
399 HgMetadata::from_metadata(meta),
399 HgMetadata::from_metadata(meta),
400 &self.dmap.copy_map,
400 &self.dmap.copy_map,
401 self.options,
401 self.options,
402 ),
402 ),
403 ));
403 ));
404 }
404 }
405 Some((
405 Some((
406 Cow::Borrowed(normalized),
406 Cow::Borrowed(normalized),
407 Dispatch::Unknown,
407 Dispatch::Unknown,
408 ))
408 ))
409 } else if file_type.is_dir() {
409 } else if file_type.is_dir() {
410 if self.options.collect_traversed_dirs {
410 if self.options.collect_traversed_dirs {
411 traversed_sender
411 traversed_sender
412 .send(normalized.to_owned())
412 .send(normalized.to_owned())
413 .expect("receiver should outlive sender");
413 .expect("receiver should outlive sender");
414 }
414 }
415 Some((
415 Some((
416 Cow::Borrowed(normalized),
416 Cow::Borrowed(normalized),
417 Dispatch::Directory {
417 Dispatch::Directory {
418 was_file: in_dmap.is_some(),
418 was_file: in_dmap.is_some(),
419 },
419 },
420 ))
420 ))
421 } else {
421 } else {
422 Some((
422 Some((
423 Cow::Borrowed(normalized),
423 Cow::Borrowed(normalized),
424 Dispatch::Bad(BadMatch::BadType(
424 Dispatch::Bad(BadMatch::BadType(
425 // TODO do more than unknown
425 // TODO do more than unknown
426 // Support for all `BadType` variant
426 // Support for all `BadType` variant
427 // varies greatly between platforms.
427 // varies greatly between platforms.
428 // So far, no tests check the type and
428 // So far, no tests check the type and
429 // this should be good enough for most
429 // this should be good enough for most
430 // users.
430 // users.
431 BadType::Unknown,
431 BadType::Unknown,
432 )),
432 )),
433 ))
433 ))
434 };
434 };
435 }
435 }
436 Err(_) => {
436 Err(_) => {
437 if let Some(entry) = in_dmap {
437 if let Some(entry) = in_dmap {
438 return Some((
438 return Some((
439 Cow::Borrowed(normalized),
439 Cow::Borrowed(normalized),
440 dispatch_missing(entry.state),
440 dispatch_missing(entry.state),
441 ));
441 ));
442 }
442 }
443 }
443 }
444 };
444 };
445 None
445 None
446 })
446 })
447 .partition(|(_, dispatch)| match dispatch {
447 .partition(|(_, dispatch)| match dispatch {
448 Dispatch::Directory { .. } => true,
448 Dispatch::Directory { .. } => true,
449 _ => false,
449 _ => false,
450 })
450 })
451 }
451 }
452
452
453 /// Walk the working directory recursively to look for changes compared to
453 /// Walk the working directory recursively to look for changes compared to
454 /// the current `DirstateMap`.
454 /// the current `DirstateMap`.
455 ///
455 ///
456 /// This takes a mutable reference to the results to account for the
456 /// This takes a mutable reference to the results to account for the
457 /// `extend` in timings
457 /// `extend` in timings
458 #[timed]
458 #[timed]
459 pub fn traverse(
459 pub fn traverse(
460 &self,
460 &self,
461 path: impl AsRef<HgPath>,
461 path: impl AsRef<HgPath>,
462 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>,
462 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>,
463 results: &mut Vec<DispatchedPath<'a>>,
463 results: &mut Vec<DispatchedPath<'a>>,
464 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
464 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
465 ) {
465 ) {
466 // The traversal is done in parallel, so use a channel to gather
466 // The traversal is done in parallel, so use a channel to gather
467 // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender`
467 // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender`
468 // is not.
468 // is not.
469 let (files_transmitter, files_receiver) =
469 let (files_transmitter, files_receiver) =
470 crossbeam_channel::unbounded();
470 crossbeam_channel::unbounded();
471
471
472 self.traverse_dir(
472 self.traverse_dir(
473 &files_transmitter,
473 &files_transmitter,
474 path,
474 path,
475 &old_results,
475 &old_results,
476 traversed_sender,
476 traversed_sender,
477 );
477 );
478
478
479 // Disconnect the channel so the receiver stops waiting
479 // Disconnect the channel so the receiver stops waiting
480 drop(files_transmitter);
480 drop(files_transmitter);
481
481
482 let new_results = files_receiver
482 let new_results = files_receiver
483 .into_iter()
483 .into_iter()
484 .par_bridge()
484 .par_bridge()
485 .map(|(f, d)| (Cow::Owned(f), d));
485 .map(|(f, d)| (Cow::Owned(f), d));
486
486
487 results.par_extend(new_results);
487 results.par_extend(new_results);
488 }
488 }
489
489
490 /// Dispatch a single entry (file, folder, symlink...) found during
490 /// Dispatch a single entry (file, folder, symlink...) found during
491 /// `traverse`. If the entry is a folder that needs to be traversed, it
491 /// `traverse`. If the entry is a folder that needs to be traversed, it
492 /// will be handled in a separate thread.
492 /// will be handled in a separate thread.
493 fn handle_traversed_entry<'b>(
493 fn handle_traversed_entry<'b>(
494 &'a self,
494 &'a self,
495 scope: &rayon::Scope<'b>,
495 scope: &rayon::Scope<'b>,
496 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
496 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
497 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
497 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
498 filename: HgPathBuf,
498 filename: HgPathBuf,
499 dir_entry: DirEntry,
499 dir_entry: DirEntry,
500 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
500 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
501 ) -> IoResult<()>
501 ) -> IoResult<()>
502 where
502 where
503 'a: 'b,
503 'a: 'b,
504 {
504 {
505 let file_type = dir_entry.file_type()?;
505 let file_type = dir_entry.file_type()?;
506 let entry_option = self.dmap.get(&filename);
506 let entry_option = self.dmap.get(&filename);
507
507
508 if filename.as_bytes() == b".hg" {
508 if filename.as_bytes() == b".hg" {
509 // Could be a directory or a symlink
509 // Could be a directory or a symlink
510 return Ok(());
510 return Ok(());
511 }
511 }
512
512
513 if file_type.is_dir() {
513 if file_type.is_dir() {
514 self.handle_traversed_dir(
514 self.handle_traversed_dir(
515 scope,
515 scope,
516 files_sender,
516 files_sender,
517 old_results,
517 old_results,
518 entry_option,
518 entry_option,
519 filename,
519 filename,
520 traversed_sender,
520 traversed_sender,
521 );
521 );
522 } else if file_type.is_file() || file_type.is_symlink() {
522 } else if file_type.is_file() || file_type.is_symlink() {
523 if let Some(entry) = entry_option {
523 if let Some(entry) = entry_option {
524 if self.matcher.matches_everything()
524 if self.matcher.matches_everything()
525 || self.matcher.matches(&filename)
525 || self.matcher.matches(&filename)
526 {
526 {
527 let metadata = dir_entry.metadata()?;
527 let metadata = dir_entry.metadata()?;
528 files_sender
528 files_sender
529 .send((
529 .send((
530 filename.to_owned(),
530 filename.to_owned(),
531 dispatch_found(
531 dispatch_found(
532 &filename,
532 &filename,
533 *entry,
533 *entry,
534 HgMetadata::from_metadata(metadata),
534 HgMetadata::from_metadata(metadata),
535 &self.dmap.copy_map,
535 &self.dmap.copy_map,
536 self.options,
536 self.options,
537 ),
537 ),
538 ))
538 ))
539 .unwrap();
539 .unwrap();
540 }
540 }
541 } else if (self.matcher.matches_everything()
541 } else if (self.matcher.matches_everything()
542 || self.matcher.matches(&filename))
542 || self.matcher.matches(&filename))
543 && !self.is_ignored(&filename)
543 && !self.is_ignored(&filename)
544 {
544 {
545 if (self.options.list_ignored
545 if (self.options.list_ignored
546 || self.matcher.exact_match(&filename))
546 || self.matcher.exact_match(&filename))
547 && self.dir_ignore(&filename)
547 && self.dir_ignore(&filename)
548 {
548 {
549 if self.options.list_ignored {
549 if self.options.list_ignored {
550 files_sender
550 files_sender
551 .send((filename.to_owned(), Dispatch::Ignored))
551 .send((filename.to_owned(), Dispatch::Ignored))
552 .unwrap();
552 .unwrap();
553 }
553 }
554 } else if self.options.list_unknown {
554 } else if self.options.list_unknown {
555 files_sender
555 files_sender
556 .send((filename.to_owned(), Dispatch::Unknown))
556 .send((filename.to_owned(), Dispatch::Unknown))
557 .unwrap();
557 .unwrap();
558 }
558 }
559 } else if self.is_ignored(&filename) && self.options.list_ignored {
559 } else if self.is_ignored(&filename) && self.options.list_ignored {
560 files_sender
560 if self.matcher.matches(&filename) {
561 .send((filename.to_owned(), Dispatch::Ignored))
561 files_sender
562 .unwrap();
562 .send((filename.to_owned(), Dispatch::Ignored))
563 .unwrap();
564 }
563 }
565 }
564 } else if let Some(entry) = entry_option {
566 } else if let Some(entry) = entry_option {
565 // Used to be a file or a folder, now something else.
567 // Used to be a file or a folder, now something else.
566 if self.matcher.matches_everything()
568 if self.matcher.matches_everything()
567 || self.matcher.matches(&filename)
569 || self.matcher.matches(&filename)
568 {
570 {
569 files_sender
571 files_sender
570 .send((filename.to_owned(), dispatch_missing(entry.state)))
572 .send((filename.to_owned(), dispatch_missing(entry.state)))
571 .unwrap();
573 .unwrap();
572 }
574 }
573 }
575 }
574
576
575 Ok(())
577 Ok(())
576 }
578 }
577
579
578 /// A directory was found in the filesystem and needs to be traversed
580 /// A directory was found in the filesystem and needs to be traversed
579 fn handle_traversed_dir<'b>(
581 fn handle_traversed_dir<'b>(
580 &'a self,
582 &'a self,
581 scope: &rayon::Scope<'b>,
583 scope: &rayon::Scope<'b>,
582 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
584 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
583 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
585 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
584 entry_option: Option<&'a DirstateEntry>,
586 entry_option: Option<&'a DirstateEntry>,
585 directory: HgPathBuf,
587 directory: HgPathBuf,
586 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
588 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
587 ) where
589 ) where
588 'a: 'b,
590 'a: 'b,
589 {
591 {
590 scope.spawn(move |_| {
592 scope.spawn(move |_| {
591 // Nested `if` until `rust-lang/rust#53668` is stable
593 // Nested `if` until `rust-lang/rust#53668` is stable
592 if let Some(entry) = entry_option {
594 if let Some(entry) = entry_option {
593 // Used to be a file, is now a folder
595 // Used to be a file, is now a folder
594 if self.matcher.matches_everything()
596 if self.matcher.matches_everything()
595 || self.matcher.matches(&directory)
597 || self.matcher.matches(&directory)
596 {
598 {
597 files_sender
599 files_sender
598 .send((
600 .send((
599 directory.to_owned(),
601 directory.to_owned(),
600 dispatch_missing(entry.state),
602 dispatch_missing(entry.state),
601 ))
603 ))
602 .unwrap();
604 .unwrap();
603 }
605 }
604 }
606 }
605 // Do we need to traverse it?
607 // Do we need to traverse it?
606 if !self.is_ignored(&directory) || self.options.list_ignored {
608 if !self.is_ignored(&directory) || self.options.list_ignored {
607 self.traverse_dir(
609 self.traverse_dir(
608 files_sender,
610 files_sender,
609 directory,
611 directory,
610 &old_results,
612 &old_results,
611 traversed_sender,
613 traversed_sender,
612 )
614 )
613 }
615 }
614 });
616 });
615 }
617 }
616
618
617 /// Decides whether the directory needs to be listed, and if so handles the
619 /// Decides whether the directory needs to be listed, and if so handles the
618 /// entries in a separate thread.
620 /// entries in a separate thread.
619 fn traverse_dir(
621 fn traverse_dir(
620 &self,
622 &self,
621 files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
623 files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
622 directory: impl AsRef<HgPath>,
624 directory: impl AsRef<HgPath>,
623 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
625 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
624 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
626 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
625 ) {
627 ) {
626 let directory = directory.as_ref();
628 let directory = directory.as_ref();
627
629
628 if self.options.collect_traversed_dirs {
630 if self.options.collect_traversed_dirs {
629 traversed_sender
631 traversed_sender
630 .send(directory.to_owned())
632 .send(directory.to_owned())
631 .expect("receiver should outlive sender");
633 .expect("receiver should outlive sender");
632 }
634 }
633
635
634 let visit_entries = match self.matcher.visit_children_set(directory) {
636 let visit_entries = match self.matcher.visit_children_set(directory) {
635 VisitChildrenSet::Empty => return,
637 VisitChildrenSet::Empty => return,
636 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
638 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
637 VisitChildrenSet::Set(set) => Some(set),
639 VisitChildrenSet::Set(set) => Some(set),
638 };
640 };
639 let buf = match hg_path_to_path_buf(directory) {
641 let buf = match hg_path_to_path_buf(directory) {
640 Ok(b) => b,
642 Ok(b) => b,
641 Err(_) => {
643 Err(_) => {
642 files_sender
644 files_sender
643 .send((directory.to_owned(), INVALID_PATH_DISPATCH))
645 .send((directory.to_owned(), INVALID_PATH_DISPATCH))
644 .expect("receiver should outlive sender");
646 .expect("receiver should outlive sender");
645 return;
647 return;
646 }
648 }
647 };
649 };
648 let dir_path = self.root_dir.join(buf);
650 let dir_path = self.root_dir.join(buf);
649
651
650 let skip_dot_hg = !directory.as_bytes().is_empty();
652 let skip_dot_hg = !directory.as_bytes().is_empty();
651 let entries = match list_directory(dir_path, skip_dot_hg) {
653 let entries = match list_directory(dir_path, skip_dot_hg) {
652 Err(e) => {
654 Err(e) => {
653 files_sender
655 files_sender
654 .send((directory.to_owned(), dispatch_os_error(&e)))
656 .send((directory.to_owned(), dispatch_os_error(&e)))
655 .expect("receiver should outlive sender");
657 .expect("receiver should outlive sender");
656 return;
658 return;
657 }
659 }
658 Ok(entries) => entries,
660 Ok(entries) => entries,
659 };
661 };
660
662
661 rayon::scope(|scope| {
663 rayon::scope(|scope| {
662 for (filename, dir_entry) in entries {
664 for (filename, dir_entry) in entries {
663 if let Some(ref set) = visit_entries {
665 if let Some(ref set) = visit_entries {
664 if !set.contains(filename.deref()) {
666 if !set.contains(filename.deref()) {
665 continue;
667 continue;
666 }
668 }
667 }
669 }
668 // TODO normalize
670 // TODO normalize
669 let filename = if directory.is_empty() {
671 let filename = if directory.is_empty() {
670 filename.to_owned()
672 filename.to_owned()
671 } else {
673 } else {
672 directory.join(&filename)
674 directory.join(&filename)
673 };
675 };
674
676
675 if !old_results.contains_key(filename.deref()) {
677 if !old_results.contains_key(filename.deref()) {
676 match self.handle_traversed_entry(
678 match self.handle_traversed_entry(
677 scope,
679 scope,
678 files_sender,
680 files_sender,
679 old_results,
681 old_results,
680 filename,
682 filename,
681 dir_entry,
683 dir_entry,
682 traversed_sender.clone(),
684 traversed_sender.clone(),
683 ) {
685 ) {
684 Err(e) => {
686 Err(e) => {
685 files_sender
687 files_sender
686 .send((
688 .send((
687 directory.to_owned(),
689 directory.to_owned(),
688 dispatch_os_error(&e),
690 dispatch_os_error(&e),
689 ))
691 ))
690 .expect("receiver should outlive sender");
692 .expect("receiver should outlive sender");
691 }
693 }
692 Ok(_) => {}
694 Ok(_) => {}
693 }
695 }
694 }
696 }
695 }
697 }
696 })
698 })
697 }
699 }
698
700
699 /// Add the files in the dirstate to the results.
701 /// Add the files in the dirstate to the results.
700 ///
702 ///
701 /// This takes a mutable reference to the results to account for the
703 /// This takes a mutable reference to the results to account for the
702 /// `extend` in timings
704 /// `extend` in timings
703 #[timed]
705 #[timed]
704 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
706 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
705 results.par_extend(
707 results.par_extend(
706 self.dmap
708 self.dmap
707 .par_iter()
709 .par_iter()
708 .filter(|(path, _)| self.matcher.matches(path))
710 .filter(|(path, _)| self.matcher.matches(path))
709 .map(move |(filename, entry)| {
711 .map(move |(filename, entry)| {
710 let filename: &HgPath = filename;
712 let filename: &HgPath = filename;
711 let filename_as_path = match hg_path_to_path_buf(filename)
713 let filename_as_path = match hg_path_to_path_buf(filename)
712 {
714 {
713 Ok(f) => f,
715 Ok(f) => f,
714 Err(_) => {
716 Err(_) => {
715 return (
717 return (
716 Cow::Borrowed(filename),
718 Cow::Borrowed(filename),
717 INVALID_PATH_DISPATCH,
719 INVALID_PATH_DISPATCH,
718 )
720 )
719 }
721 }
720 };
722 };
721 let meta = self
723 let meta = self
722 .root_dir
724 .root_dir
723 .join(filename_as_path)
725 .join(filename_as_path)
724 .symlink_metadata();
726 .symlink_metadata();
725 match meta {
727 match meta {
726 Ok(m)
728 Ok(m)
727 if !(m.file_type().is_file()
729 if !(m.file_type().is_file()
728 || m.file_type().is_symlink()) =>
730 || m.file_type().is_symlink()) =>
729 {
731 {
730 (
732 (
731 Cow::Borrowed(filename),
733 Cow::Borrowed(filename),
732 dispatch_missing(entry.state),
734 dispatch_missing(entry.state),
733 )
735 )
734 }
736 }
735 Ok(m) => (
737 Ok(m) => (
736 Cow::Borrowed(filename),
738 Cow::Borrowed(filename),
737 dispatch_found(
739 dispatch_found(
738 filename,
740 filename,
739 *entry,
741 *entry,
740 HgMetadata::from_metadata(m),
742 HgMetadata::from_metadata(m),
741 &self.dmap.copy_map,
743 &self.dmap.copy_map,
742 self.options,
744 self.options,
743 ),
745 ),
744 ),
746 ),
745 Err(e)
747 Err(e)
746 if e.kind() == ErrorKind::NotFound
748 if e.kind() == ErrorKind::NotFound
747 || e.raw_os_error() == Some(20) =>
749 || e.raw_os_error() == Some(20) =>
748 {
750 {
749 // Rust does not yet have an `ErrorKind` for
751 // Rust does not yet have an `ErrorKind` for
750 // `NotADirectory` (errno 20)
752 // `NotADirectory` (errno 20)
751 // It happens if the dirstate contains `foo/bar`
753 // It happens if the dirstate contains `foo/bar`
752 // and foo is not a
754 // and foo is not a
753 // directory
755 // directory
754 (
756 (
755 Cow::Borrowed(filename),
757 Cow::Borrowed(filename),
756 dispatch_missing(entry.state),
758 dispatch_missing(entry.state),
757 )
759 )
758 }
760 }
759 Err(e) => {
761 Err(e) => {
760 (Cow::Borrowed(filename), dispatch_os_error(&e))
762 (Cow::Borrowed(filename), dispatch_os_error(&e))
761 }
763 }
762 }
764 }
763 }),
765 }),
764 );
766 );
765 }
767 }
766
768
767 /// Checks all files that are in the dirstate but were not found during the
769 /// Checks all files that are in the dirstate but were not found during the
768 /// working directory traversal. This means that the rest must
770 /// working directory traversal. This means that the rest must
769 /// be either ignored, under a symlink or under a new nested repo.
771 /// be either ignored, under a symlink or under a new nested repo.
770 ///
772 ///
771 /// This takes a mutable reference to the results to account for the
773 /// This takes a mutable reference to the results to account for the
772 /// `extend` in timings
774 /// `extend` in timings
773 #[timed]
775 #[timed]
774 pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) {
776 pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) {
775 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
777 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
776 if results.is_empty() && self.matcher.matches_everything() {
778 if results.is_empty() && self.matcher.matches_everything() {
777 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
779 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
778 } else {
780 } else {
779 // Only convert to a hashmap if needed.
781 // Only convert to a hashmap if needed.
780 let old_results: FastHashMap<_, _> =
782 let old_results: FastHashMap<_, _> =
781 results.iter().cloned().collect();
783 results.iter().cloned().collect();
782 self.dmap
784 self.dmap
783 .iter()
785 .iter()
784 .filter_map(move |(f, e)| {
786 .filter_map(move |(f, e)| {
785 if !old_results.contains_key(f.deref())
787 if !old_results.contains_key(f.deref())
786 && self.matcher.matches(f)
788 && self.matcher.matches(f)
787 {
789 {
788 Some((f.deref(), e))
790 Some((f.deref(), e))
789 } else {
791 } else {
790 None
792 None
791 }
793 }
792 })
794 })
793 .collect()
795 .collect()
794 };
796 };
795
797
796 let path_auditor = PathAuditor::new(&self.root_dir);
798 let path_auditor = PathAuditor::new(&self.root_dir);
797
799
798 let new_results = to_visit.into_par_iter().filter_map(
800 let new_results = to_visit.into_par_iter().filter_map(
799 |(filename, entry)| -> Option<_> {
801 |(filename, entry)| -> Option<_> {
800 // Report ignored items in the dmap as long as they are not
802 // Report ignored items in the dmap as long as they are not
801 // under a symlink directory.
803 // under a symlink directory.
802 if path_auditor.check(filename) {
804 if path_auditor.check(filename) {
803 // TODO normalize for case-insensitive filesystems
805 // TODO normalize for case-insensitive filesystems
804 let buf = match hg_path_to_path_buf(filename) {
806 let buf = match hg_path_to_path_buf(filename) {
805 Ok(x) => x,
807 Ok(x) => x,
806 Err(_) => {
808 Err(_) => {
807 return Some((
809 return Some((
808 Cow::Owned(filename.to_owned()),
810 Cow::Owned(filename.to_owned()),
809 INVALID_PATH_DISPATCH,
811 INVALID_PATH_DISPATCH,
810 ));
812 ));
811 }
813 }
812 };
814 };
813 Some((
815 Some((
814 Cow::Owned(filename.to_owned()),
816 Cow::Owned(filename.to_owned()),
815 match self.root_dir.join(&buf).symlink_metadata() {
817 match self.root_dir.join(&buf).symlink_metadata() {
816 // File was just ignored, no links, and exists
818 // File was just ignored, no links, and exists
817 Ok(meta) => {
819 Ok(meta) => {
818 let metadata = HgMetadata::from_metadata(meta);
820 let metadata = HgMetadata::from_metadata(meta);
819 dispatch_found(
821 dispatch_found(
820 filename,
822 filename,
821 *entry,
823 *entry,
822 metadata,
824 metadata,
823 &self.dmap.copy_map,
825 &self.dmap.copy_map,
824 self.options,
826 self.options,
825 )
827 )
826 }
828 }
827 // File doesn't exist
829 // File doesn't exist
828 Err(_) => dispatch_missing(entry.state),
830 Err(_) => dispatch_missing(entry.state),
829 },
831 },
830 ))
832 ))
831 } else {
833 } else {
832 // It's either missing or under a symlink directory which
834 // It's either missing or under a symlink directory which
833 // we, in this case, report as missing.
835 // we, in this case, report as missing.
834 Some((
836 Some((
835 Cow::Owned(filename.to_owned()),
837 Cow::Owned(filename.to_owned()),
836 dispatch_missing(entry.state),
838 dispatch_missing(entry.state),
837 ))
839 ))
838 }
840 }
839 },
841 },
840 );
842 );
841
843
842 results.par_extend(new_results);
844 results.par_extend(new_results);
843 }
845 }
844 }
846 }
845
847
846 #[timed]
848 #[timed]
847 pub fn build_response<'a>(
849 pub fn build_response<'a>(
848 results: impl IntoIterator<Item = DispatchedPath<'a>>,
850 results: impl IntoIterator<Item = DispatchedPath<'a>>,
849 traversed: Vec<HgPathBuf>,
851 traversed: Vec<HgPathBuf>,
850 ) -> (Vec<HgPathCow<'a>>, DirstateStatus<'a>) {
852 ) -> (Vec<HgPathCow<'a>>, DirstateStatus<'a>) {
851 let mut lookup = vec![];
853 let mut lookup = vec![];
852 let mut modified = vec![];
854 let mut modified = vec![];
853 let mut added = vec![];
855 let mut added = vec![];
854 let mut removed = vec![];
856 let mut removed = vec![];
855 let mut deleted = vec![];
857 let mut deleted = vec![];
856 let mut clean = vec![];
858 let mut clean = vec![];
857 let mut ignored = vec![];
859 let mut ignored = vec![];
858 let mut unknown = vec![];
860 let mut unknown = vec![];
859 let mut bad = vec![];
861 let mut bad = vec![];
860
862
861 for (filename, dispatch) in results.into_iter() {
863 for (filename, dispatch) in results.into_iter() {
862 match dispatch {
864 match dispatch {
863 Dispatch::Unknown => unknown.push(filename),
865 Dispatch::Unknown => unknown.push(filename),
864 Dispatch::Unsure => lookup.push(filename),
866 Dispatch::Unsure => lookup.push(filename),
865 Dispatch::Modified => modified.push(filename),
867 Dispatch::Modified => modified.push(filename),
866 Dispatch::Added => added.push(filename),
868 Dispatch::Added => added.push(filename),
867 Dispatch::Removed => removed.push(filename),
869 Dispatch::Removed => removed.push(filename),
868 Dispatch::Deleted => deleted.push(filename),
870 Dispatch::Deleted => deleted.push(filename),
869 Dispatch::Clean => clean.push(filename),
871 Dispatch::Clean => clean.push(filename),
870 Dispatch::Ignored => ignored.push(filename),
872 Dispatch::Ignored => ignored.push(filename),
871 Dispatch::None => {}
873 Dispatch::None => {}
872 Dispatch::Bad(reason) => bad.push((filename, reason)),
874 Dispatch::Bad(reason) => bad.push((filename, reason)),
873 Dispatch::Directory { .. } => {}
875 Dispatch::Directory { .. } => {}
874 }
876 }
875 }
877 }
876
878
877 (
879 (
878 lookup,
880 lookup,
879 DirstateStatus {
881 DirstateStatus {
880 modified,
882 modified,
881 added,
883 added,
882 removed,
884 removed,
883 deleted,
885 deleted,
884 clean,
886 clean,
885 ignored,
887 ignored,
886 unknown,
888 unknown,
887 bad,
889 bad,
888 traversed,
890 traversed,
889 },
891 },
890 )
892 )
891 }
893 }
892
894
893 /// Get the status of files in the working directory.
895 /// Get the status of files in the working directory.
894 ///
896 ///
895 /// This is the current entry-point for `hg-core` and is realistically unusable
897 /// This is the current entry-point for `hg-core` and is realistically unusable
896 /// outside of a Python context because its arguments need to provide a lot of
898 /// outside of a Python context because its arguments need to provide a lot of
897 /// information that will not be necessary in the future.
899 /// information that will not be necessary in the future.
898 #[timed]
900 #[timed]
899 pub fn status<'a>(
901 pub fn status<'a>(
900 dmap: &'a DirstateMap,
902 dmap: &'a DirstateMap,
901 matcher: &'a (impl Matcher + Sync),
903 matcher: &'a (impl Matcher + Sync),
902 root_dir: PathBuf,
904 root_dir: PathBuf,
903 ignore_files: Vec<PathBuf>,
905 ignore_files: Vec<PathBuf>,
904 options: StatusOptions,
906 options: StatusOptions,
905 ) -> StatusResult<(
907 ) -> StatusResult<(
906 (Vec<HgPathCow<'a>>, DirstateStatus<'a>),
908 (Vec<HgPathCow<'a>>, DirstateStatus<'a>),
907 Vec<PatternFileWarning>,
909 Vec<PatternFileWarning>,
908 )> {
910 )> {
909 let (status, warnings) =
911 let (status, warnings) =
910 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
912 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
911
913
912 Ok((status.run()?, warnings))
914 Ok((status.run()?, warnings))
913 }
915 }
@@ -1,711 +1,710 b''
1 $ hg init repo1
1 $ hg init repo1
2 $ cd repo1
2 $ cd repo1
3 $ mkdir a b a/1 b/1 b/2
3 $ mkdir a b a/1 b/1 b/2
4 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
4 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
5
5
6 hg status in repo root:
6 hg status in repo root:
7
7
8 $ hg status
8 $ hg status
9 ? a/1/in_a_1
9 ? a/1/in_a_1
10 ? a/in_a
10 ? a/in_a
11 ? b/1/in_b_1
11 ? b/1/in_b_1
12 ? b/2/in_b_2
12 ? b/2/in_b_2
13 ? b/in_b
13 ? b/in_b
14 ? in_root
14 ? in_root
15
15
16 hg status . in repo root:
16 hg status . in repo root:
17
17
18 $ hg status .
18 $ hg status .
19 ? a/1/in_a_1
19 ? a/1/in_a_1
20 ? a/in_a
20 ? a/in_a
21 ? b/1/in_b_1
21 ? b/1/in_b_1
22 ? b/2/in_b_2
22 ? b/2/in_b_2
23 ? b/in_b
23 ? b/in_b
24 ? in_root
24 ? in_root
25
25
26 $ hg status --cwd a
26 $ hg status --cwd a
27 ? a/1/in_a_1
27 ? a/1/in_a_1
28 ? a/in_a
28 ? a/in_a
29 ? b/1/in_b_1
29 ? b/1/in_b_1
30 ? b/2/in_b_2
30 ? b/2/in_b_2
31 ? b/in_b
31 ? b/in_b
32 ? in_root
32 ? in_root
33 $ hg status --cwd a .
33 $ hg status --cwd a .
34 ? 1/in_a_1
34 ? 1/in_a_1
35 ? in_a
35 ? in_a
36 $ hg status --cwd a ..
36 $ hg status --cwd a ..
37 ? 1/in_a_1
37 ? 1/in_a_1
38 ? in_a
38 ? in_a
39 ? ../b/1/in_b_1
39 ? ../b/1/in_b_1
40 ? ../b/2/in_b_2
40 ? ../b/2/in_b_2
41 ? ../b/in_b
41 ? ../b/in_b
42 ? ../in_root
42 ? ../in_root
43
43
44 $ hg status --cwd b
44 $ hg status --cwd b
45 ? a/1/in_a_1
45 ? a/1/in_a_1
46 ? a/in_a
46 ? a/in_a
47 ? b/1/in_b_1
47 ? b/1/in_b_1
48 ? b/2/in_b_2
48 ? b/2/in_b_2
49 ? b/in_b
49 ? b/in_b
50 ? in_root
50 ? in_root
51 $ hg status --cwd b .
51 $ hg status --cwd b .
52 ? 1/in_b_1
52 ? 1/in_b_1
53 ? 2/in_b_2
53 ? 2/in_b_2
54 ? in_b
54 ? in_b
55 $ hg status --cwd b ..
55 $ hg status --cwd b ..
56 ? ../a/1/in_a_1
56 ? ../a/1/in_a_1
57 ? ../a/in_a
57 ? ../a/in_a
58 ? 1/in_b_1
58 ? 1/in_b_1
59 ? 2/in_b_2
59 ? 2/in_b_2
60 ? in_b
60 ? in_b
61 ? ../in_root
61 ? ../in_root
62
62
63 $ hg status --cwd a/1
63 $ hg status --cwd a/1
64 ? a/1/in_a_1
64 ? a/1/in_a_1
65 ? a/in_a
65 ? a/in_a
66 ? b/1/in_b_1
66 ? b/1/in_b_1
67 ? b/2/in_b_2
67 ? b/2/in_b_2
68 ? b/in_b
68 ? b/in_b
69 ? in_root
69 ? in_root
70 $ hg status --cwd a/1 .
70 $ hg status --cwd a/1 .
71 ? in_a_1
71 ? in_a_1
72 $ hg status --cwd a/1 ..
72 $ hg status --cwd a/1 ..
73 ? in_a_1
73 ? in_a_1
74 ? ../in_a
74 ? ../in_a
75
75
76 $ hg status --cwd b/1
76 $ hg status --cwd b/1
77 ? a/1/in_a_1
77 ? a/1/in_a_1
78 ? a/in_a
78 ? a/in_a
79 ? b/1/in_b_1
79 ? b/1/in_b_1
80 ? b/2/in_b_2
80 ? b/2/in_b_2
81 ? b/in_b
81 ? b/in_b
82 ? in_root
82 ? in_root
83 $ hg status --cwd b/1 .
83 $ hg status --cwd b/1 .
84 ? in_b_1
84 ? in_b_1
85 $ hg status --cwd b/1 ..
85 $ hg status --cwd b/1 ..
86 ? in_b_1
86 ? in_b_1
87 ? ../2/in_b_2
87 ? ../2/in_b_2
88 ? ../in_b
88 ? ../in_b
89
89
90 $ hg status --cwd b/2
90 $ hg status --cwd b/2
91 ? a/1/in_a_1
91 ? a/1/in_a_1
92 ? a/in_a
92 ? a/in_a
93 ? b/1/in_b_1
93 ? b/1/in_b_1
94 ? b/2/in_b_2
94 ? b/2/in_b_2
95 ? b/in_b
95 ? b/in_b
96 ? in_root
96 ? in_root
97 $ hg status --cwd b/2 .
97 $ hg status --cwd b/2 .
98 ? in_b_2
98 ? in_b_2
99 $ hg status --cwd b/2 ..
99 $ hg status --cwd b/2 ..
100 ? ../1/in_b_1
100 ? ../1/in_b_1
101 ? in_b_2
101 ? in_b_2
102 ? ../in_b
102 ? ../in_b
103
103
104 combining patterns with root and patterns without a root works
104 combining patterns with root and patterns without a root works
105
105
106 $ hg st a/in_a re:.*b$
106 $ hg st a/in_a re:.*b$
107 ? a/in_a
107 ? a/in_a
108 ? b/in_b
108 ? b/in_b
109
109
110 tweaking defaults works
110 tweaking defaults works
111 $ hg status --cwd a --config ui.tweakdefaults=yes
111 $ hg status --cwd a --config ui.tweakdefaults=yes
112 ? 1/in_a_1
112 ? 1/in_a_1
113 ? in_a
113 ? in_a
114 ? ../b/1/in_b_1
114 ? ../b/1/in_b_1
115 ? ../b/2/in_b_2
115 ? ../b/2/in_b_2
116 ? ../b/in_b
116 ? ../b/in_b
117 ? ../in_root
117 ? ../in_root
118 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
118 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
119 ? a/1/in_a_1 (glob)
119 ? a/1/in_a_1 (glob)
120 ? a/in_a (glob)
120 ? a/in_a (glob)
121 ? b/1/in_b_1 (glob)
121 ? b/1/in_b_1 (glob)
122 ? b/2/in_b_2 (glob)
122 ? b/2/in_b_2 (glob)
123 ? b/in_b (glob)
123 ? b/in_b (glob)
124 ? in_root
124 ? in_root
125 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
125 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
126 ? 1/in_a_1
126 ? 1/in_a_1
127 ? in_a
127 ? in_a
128 ? ../b/1/in_b_1
128 ? ../b/1/in_b_1
129 ? ../b/2/in_b_2
129 ? ../b/2/in_b_2
130 ? ../b/in_b
130 ? ../b/in_b
131 ? ../in_root (glob)
131 ? ../in_root (glob)
132
132
133 relative paths can be requested
133 relative paths can be requested
134
134
135 $ hg status --cwd a --config ui.relative-paths=yes
135 $ hg status --cwd a --config ui.relative-paths=yes
136 ? 1/in_a_1
136 ? 1/in_a_1
137 ? in_a
137 ? in_a
138 ? ../b/1/in_b_1
138 ? ../b/1/in_b_1
139 ? ../b/2/in_b_2
139 ? ../b/2/in_b_2
140 ? ../b/in_b
140 ? ../b/in_b
141 ? ../in_root
141 ? ../in_root
142
142
143 $ hg status --cwd a . --config ui.relative-paths=legacy
143 $ hg status --cwd a . --config ui.relative-paths=legacy
144 ? 1/in_a_1
144 ? 1/in_a_1
145 ? in_a
145 ? in_a
146 $ hg status --cwd a . --config ui.relative-paths=no
146 $ hg status --cwd a . --config ui.relative-paths=no
147 ? a/1/in_a_1
147 ? a/1/in_a_1
148 ? a/in_a
148 ? a/in_a
149
149
150 commands.status.relative overrides ui.relative-paths
150 commands.status.relative overrides ui.relative-paths
151
151
152 $ cat >> $HGRCPATH <<EOF
152 $ cat >> $HGRCPATH <<EOF
153 > [ui]
153 > [ui]
154 > relative-paths = False
154 > relative-paths = False
155 > [commands]
155 > [commands]
156 > status.relative = True
156 > status.relative = True
157 > EOF
157 > EOF
158 $ hg status --cwd a
158 $ hg status --cwd a
159 ? 1/in_a_1
159 ? 1/in_a_1
160 ? in_a
160 ? in_a
161 ? ../b/1/in_b_1
161 ? ../b/1/in_b_1
162 ? ../b/2/in_b_2
162 ? ../b/2/in_b_2
163 ? ../b/in_b
163 ? ../b/in_b
164 ? ../in_root
164 ? ../in_root
165 $ HGPLAIN=1 hg status --cwd a
165 $ HGPLAIN=1 hg status --cwd a
166 ? a/1/in_a_1 (glob)
166 ? a/1/in_a_1 (glob)
167 ? a/in_a (glob)
167 ? a/in_a (glob)
168 ? b/1/in_b_1 (glob)
168 ? b/1/in_b_1 (glob)
169 ? b/2/in_b_2 (glob)
169 ? b/2/in_b_2 (glob)
170 ? b/in_b (glob)
170 ? b/in_b (glob)
171 ? in_root
171 ? in_root
172
172
173 if relative paths are explicitly off, tweakdefaults doesn't change it
173 if relative paths are explicitly off, tweakdefaults doesn't change it
174 $ cat >> $HGRCPATH <<EOF
174 $ cat >> $HGRCPATH <<EOF
175 > [commands]
175 > [commands]
176 > status.relative = False
176 > status.relative = False
177 > EOF
177 > EOF
178 $ hg status --cwd a --config ui.tweakdefaults=yes
178 $ hg status --cwd a --config ui.tweakdefaults=yes
179 ? a/1/in_a_1
179 ? a/1/in_a_1
180 ? a/in_a
180 ? a/in_a
181 ? b/1/in_b_1
181 ? b/1/in_b_1
182 ? b/2/in_b_2
182 ? b/2/in_b_2
183 ? b/in_b
183 ? b/in_b
184 ? in_root
184 ? in_root
185
185
186 $ cd ..
186 $ cd ..
187
187
188 $ hg init repo2
188 $ hg init repo2
189 $ cd repo2
189 $ cd repo2
190 $ touch modified removed deleted ignored
190 $ touch modified removed deleted ignored
191 $ echo "^ignored$" > .hgignore
191 $ echo "^ignored$" > .hgignore
192 $ hg ci -A -m 'initial checkin'
192 $ hg ci -A -m 'initial checkin'
193 adding .hgignore
193 adding .hgignore
194 adding deleted
194 adding deleted
195 adding modified
195 adding modified
196 adding removed
196 adding removed
197 $ touch modified added unknown ignored
197 $ touch modified added unknown ignored
198 $ hg add added
198 $ hg add added
199 $ hg remove removed
199 $ hg remove removed
200 $ rm deleted
200 $ rm deleted
201
201
202 hg status:
202 hg status:
203
203
204 $ hg status
204 $ hg status
205 A added
205 A added
206 R removed
206 R removed
207 ! deleted
207 ! deleted
208 ? unknown
208 ? unknown
209
209
210 hg status modified added removed deleted unknown never-existed ignored:
210 hg status modified added removed deleted unknown never-existed ignored:
211
211
212 $ hg status modified added removed deleted unknown never-existed ignored
212 $ hg status modified added removed deleted unknown never-existed ignored
213 never-existed: * (glob)
213 never-existed: * (glob)
214 A added
214 A added
215 R removed
215 R removed
216 ! deleted
216 ! deleted
217 ? unknown
217 ? unknown
218
218
219 $ hg copy modified copied
219 $ hg copy modified copied
220
220
221 hg status -C:
221 hg status -C:
222
222
223 $ hg status -C
223 $ hg status -C
224 A added
224 A added
225 A copied
225 A copied
226 modified
226 modified
227 R removed
227 R removed
228 ! deleted
228 ! deleted
229 ? unknown
229 ? unknown
230
230
231 hg status -A:
231 hg status -A:
232
232
233 $ hg status -A
233 $ hg status -A
234 A added
234 A added
235 A copied
235 A copied
236 modified
236 modified
237 R removed
237 R removed
238 ! deleted
238 ! deleted
239 ? unknown
239 ? unknown
240 I ignored
240 I ignored
241 C .hgignore
241 C .hgignore
242 C modified
242 C modified
243
243
244 $ hg status -A -T '{status} {path} {node|shortest}\n'
244 $ hg status -A -T '{status} {path} {node|shortest}\n'
245 A added ffff
245 A added ffff
246 A copied ffff
246 A copied ffff
247 R removed ffff
247 R removed ffff
248 ! deleted ffff
248 ! deleted ffff
249 ? unknown ffff
249 ? unknown ffff
250 I ignored ffff
250 I ignored ffff
251 C .hgignore ffff
251 C .hgignore ffff
252 C modified ffff
252 C modified ffff
253
253
254 $ hg status -A -Tjson
254 $ hg status -A -Tjson
255 [
255 [
256 {
256 {
257 "itemtype": "file",
257 "itemtype": "file",
258 "path": "added",
258 "path": "added",
259 "status": "A"
259 "status": "A"
260 },
260 },
261 {
261 {
262 "itemtype": "file",
262 "itemtype": "file",
263 "path": "copied",
263 "path": "copied",
264 "source": "modified",
264 "source": "modified",
265 "status": "A"
265 "status": "A"
266 },
266 },
267 {
267 {
268 "itemtype": "file",
268 "itemtype": "file",
269 "path": "removed",
269 "path": "removed",
270 "status": "R"
270 "status": "R"
271 },
271 },
272 {
272 {
273 "itemtype": "file",
273 "itemtype": "file",
274 "path": "deleted",
274 "path": "deleted",
275 "status": "!"
275 "status": "!"
276 },
276 },
277 {
277 {
278 "itemtype": "file",
278 "itemtype": "file",
279 "path": "unknown",
279 "path": "unknown",
280 "status": "?"
280 "status": "?"
281 },
281 },
282 {
282 {
283 "itemtype": "file",
283 "itemtype": "file",
284 "path": "ignored",
284 "path": "ignored",
285 "status": "I"
285 "status": "I"
286 },
286 },
287 {
287 {
288 "itemtype": "file",
288 "itemtype": "file",
289 "path": ".hgignore",
289 "path": ".hgignore",
290 "status": "C"
290 "status": "C"
291 },
291 },
292 {
292 {
293 "itemtype": "file",
293 "itemtype": "file",
294 "path": "modified",
294 "path": "modified",
295 "status": "C"
295 "status": "C"
296 }
296 }
297 ]
297 ]
298
298
299 $ hg status -A -Tpickle > pickle
299 $ hg status -A -Tpickle > pickle
300 >>> from __future__ import print_function
300 >>> from __future__ import print_function
301 >>> from mercurial import util
301 >>> from mercurial import util
302 >>> pickle = util.pickle
302 >>> pickle = util.pickle
303 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
303 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
304 >>> for s, p in data: print("%s %s" % (s, p))
304 >>> for s, p in data: print("%s %s" % (s, p))
305 ! deleted
305 ! deleted
306 ? pickle
306 ? pickle
307 ? unknown
307 ? unknown
308 A added
308 A added
309 A copied
309 A copied
310 C .hgignore
310 C .hgignore
311 C modified
311 C modified
312 I ignored
312 I ignored
313 R removed
313 R removed
314 $ rm pickle
314 $ rm pickle
315
315
316 $ echo "^ignoreddir$" > .hgignore
316 $ echo "^ignoreddir$" > .hgignore
317 $ mkdir ignoreddir
317 $ mkdir ignoreddir
318 $ touch ignoreddir/file
318 $ touch ignoreddir/file
319
319
320 Test templater support:
320 Test templater support:
321
321
322 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
322 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
323 [M] .hgignore
323 [M] .hgignore
324 [A] added
324 [A] added
325 [A] modified -> copied
325 [A] modified -> copied
326 [R] removed
326 [R] removed
327 [!] deleted
327 [!] deleted
328 [?] ignored
328 [?] ignored
329 [?] unknown
329 [?] unknown
330 [I] ignoreddir/file
330 [I] ignoreddir/file
331 [C] modified
331 [C] modified
332 $ hg status -AT default
332 $ hg status -AT default
333 M .hgignore
333 M .hgignore
334 A added
334 A added
335 A copied
335 A copied
336 modified
336 modified
337 R removed
337 R removed
338 ! deleted
338 ! deleted
339 ? ignored
339 ? ignored
340 ? unknown
340 ? unknown
341 I ignoreddir/file
341 I ignoreddir/file
342 C modified
342 C modified
343 $ hg status -T compact
343 $ hg status -T compact
344 abort: "status" not in template map
344 abort: "status" not in template map
345 [255]
345 [255]
346
346
347 hg status ignoreddir/file:
347 hg status ignoreddir/file:
348
348
349 $ hg status ignoreddir/file
349 $ hg status ignoreddir/file
350
350
351 hg status -i ignoreddir/file:
351 hg status -i ignoreddir/file:
352
352
353 $ hg status -i ignoreddir/file
353 $ hg status -i ignoreddir/file
354 I ignoreddir/file
354 I ignoreddir/file
355 $ cd ..
355 $ cd ..
356
356
357 Check 'status -q' and some combinations
357 Check 'status -q' and some combinations
358
358
359 $ hg init repo3
359 $ hg init repo3
360 $ cd repo3
360 $ cd repo3
361 $ touch modified removed deleted ignored
361 $ touch modified removed deleted ignored
362 $ echo "^ignored$" > .hgignore
362 $ echo "^ignored$" > .hgignore
363 $ hg commit -A -m 'initial checkin'
363 $ hg commit -A -m 'initial checkin'
364 adding .hgignore
364 adding .hgignore
365 adding deleted
365 adding deleted
366 adding modified
366 adding modified
367 adding removed
367 adding removed
368 $ touch added unknown ignored
368 $ touch added unknown ignored
369 $ hg add added
369 $ hg add added
370 $ echo "test" >> modified
370 $ echo "test" >> modified
371 $ hg remove removed
371 $ hg remove removed
372 $ rm deleted
372 $ rm deleted
373 $ hg copy modified copied
373 $ hg copy modified copied
374
374
375 Specify working directory revision explicitly, that should be the same as
375 Specify working directory revision explicitly, that should be the same as
376 "hg status"
376 "hg status"
377
377
378 $ hg status --change "wdir()"
378 $ hg status --change "wdir()"
379 M modified
379 M modified
380 A added
380 A added
381 A copied
381 A copied
382 R removed
382 R removed
383 ! deleted
383 ! deleted
384 ? unknown
384 ? unknown
385
385
386 Run status with 2 different flags.
386 Run status with 2 different flags.
387 Check if result is the same or different.
387 Check if result is the same or different.
388 If result is not as expected, raise error
388 If result is not as expected, raise error
389
389
390 $ assert() {
390 $ assert() {
391 > hg status $1 > ../a
391 > hg status $1 > ../a
392 > hg status $2 > ../b
392 > hg status $2 > ../b
393 > if diff ../a ../b > /dev/null; then
393 > if diff ../a ../b > /dev/null; then
394 > out=0
394 > out=0
395 > else
395 > else
396 > out=1
396 > out=1
397 > fi
397 > fi
398 > if [ $3 -eq 0 ]; then
398 > if [ $3 -eq 0 ]; then
399 > df="same"
399 > df="same"
400 > else
400 > else
401 > df="different"
401 > df="different"
402 > fi
402 > fi
403 > if [ $out -ne $3 ]; then
403 > if [ $out -ne $3 ]; then
404 > echo "Error on $1 and $2, should be $df."
404 > echo "Error on $1 and $2, should be $df."
405 > fi
405 > fi
406 > }
406 > }
407
407
408 Assert flag1 flag2 [0-same | 1-different]
408 Assert flag1 flag2 [0-same | 1-different]
409
409
410 $ assert "-q" "-mard" 0
410 $ assert "-q" "-mard" 0
411 $ assert "-A" "-marduicC" 0
411 $ assert "-A" "-marduicC" 0
412 $ assert "-qA" "-mardcC" 0
412 $ assert "-qA" "-mardcC" 0
413 $ assert "-qAui" "-A" 0
413 $ assert "-qAui" "-A" 0
414 $ assert "-qAu" "-marducC" 0
414 $ assert "-qAu" "-marducC" 0
415 $ assert "-qAi" "-mardicC" 0
415 $ assert "-qAi" "-mardicC" 0
416 $ assert "-qu" "-u" 0
416 $ assert "-qu" "-u" 0
417 $ assert "-q" "-u" 1
417 $ assert "-q" "-u" 1
418 $ assert "-m" "-a" 1
418 $ assert "-m" "-a" 1
419 $ assert "-r" "-d" 1
419 $ assert "-r" "-d" 1
420 $ cd ..
420 $ cd ..
421
421
422 $ hg init repo4
422 $ hg init repo4
423 $ cd repo4
423 $ cd repo4
424 $ touch modified removed deleted
424 $ touch modified removed deleted
425 $ hg ci -q -A -m 'initial checkin'
425 $ hg ci -q -A -m 'initial checkin'
426 $ touch added unknown
426 $ touch added unknown
427 $ hg add added
427 $ hg add added
428 $ hg remove removed
428 $ hg remove removed
429 $ rm deleted
429 $ rm deleted
430 $ echo x > modified
430 $ echo x > modified
431 $ hg copy modified copied
431 $ hg copy modified copied
432 $ hg ci -m 'test checkin' -d "1000001 0"
432 $ hg ci -m 'test checkin' -d "1000001 0"
433 $ rm *
433 $ rm *
434 $ touch unrelated
434 $ touch unrelated
435 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
435 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
436
436
437 hg status --change 1:
437 hg status --change 1:
438
438
439 $ hg status --change 1
439 $ hg status --change 1
440 M modified
440 M modified
441 A added
441 A added
442 A copied
442 A copied
443 R removed
443 R removed
444
444
445 hg status --change 1 unrelated:
445 hg status --change 1 unrelated:
446
446
447 $ hg status --change 1 unrelated
447 $ hg status --change 1 unrelated
448
448
449 hg status -C --change 1 added modified copied removed deleted:
449 hg status -C --change 1 added modified copied removed deleted:
450
450
451 $ hg status -C --change 1 added modified copied removed deleted
451 $ hg status -C --change 1 added modified copied removed deleted
452 M modified
452 M modified
453 A added
453 A added
454 A copied
454 A copied
455 modified
455 modified
456 R removed
456 R removed
457
457
458 hg status -A --change 1 and revset:
458 hg status -A --change 1 and revset:
459
459
460 $ hg status -A --change '1|1'
460 $ hg status -A --change '1|1'
461 M modified
461 M modified
462 A added
462 A added
463 A copied
463 A copied
464 modified
464 modified
465 R removed
465 R removed
466 C deleted
466 C deleted
467
467
468 $ cd ..
468 $ cd ..
469
469
470 hg status with --rev and reverted changes:
470 hg status with --rev and reverted changes:
471
471
472 $ hg init reverted-changes-repo
472 $ hg init reverted-changes-repo
473 $ cd reverted-changes-repo
473 $ cd reverted-changes-repo
474 $ echo a > file
474 $ echo a > file
475 $ hg add file
475 $ hg add file
476 $ hg ci -m a
476 $ hg ci -m a
477 $ echo b > file
477 $ echo b > file
478 $ hg ci -m b
478 $ hg ci -m b
479
479
480 reverted file should appear clean
480 reverted file should appear clean
481
481
482 $ hg revert -r 0 .
482 $ hg revert -r 0 .
483 reverting file
483 reverting file
484 $ hg status -A --rev 0
484 $ hg status -A --rev 0
485 C file
485 C file
486
486
487 #if execbit
487 #if execbit
488 reverted file with changed flag should appear modified
488 reverted file with changed flag should appear modified
489
489
490 $ chmod +x file
490 $ chmod +x file
491 $ hg status -A --rev 0
491 $ hg status -A --rev 0
492 M file
492 M file
493
493
494 $ hg revert -r 0 .
494 $ hg revert -r 0 .
495 reverting file
495 reverting file
496
496
497 reverted and committed file with changed flag should appear modified
497 reverted and committed file with changed flag should appear modified
498
498
499 $ hg co -C .
499 $ hg co -C .
500 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
500 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
501 $ chmod +x file
501 $ chmod +x file
502 $ hg ci -m 'change flag'
502 $ hg ci -m 'change flag'
503 $ hg status -A --rev 1 --rev 2
503 $ hg status -A --rev 1 --rev 2
504 M file
504 M file
505 $ hg diff -r 1 -r 2
505 $ hg diff -r 1 -r 2
506
506
507 #endif
507 #endif
508
508
509 $ cd ..
509 $ cd ..
510
510
511 hg status of binary file starting with '\1\n', a separator for metadata:
511 hg status of binary file starting with '\1\n', a separator for metadata:
512
512
513 $ hg init repo5
513 $ hg init repo5
514 $ cd repo5
514 $ cd repo5
515 >>> open("010a", r"wb").write(b"\1\nfoo") and None
515 >>> open("010a", r"wb").write(b"\1\nfoo") and None
516 $ hg ci -q -A -m 'initial checkin'
516 $ hg ci -q -A -m 'initial checkin'
517 $ hg status -A
517 $ hg status -A
518 C 010a
518 C 010a
519
519
520 >>> open("010a", r"wb").write(b"\1\nbar") and None
520 >>> open("010a", r"wb").write(b"\1\nbar") and None
521 $ hg status -A
521 $ hg status -A
522 M 010a
522 M 010a
523 $ hg ci -q -m 'modify 010a'
523 $ hg ci -q -m 'modify 010a'
524 $ hg status -A --rev 0:1
524 $ hg status -A --rev 0:1
525 M 010a
525 M 010a
526
526
527 $ touch empty
527 $ touch empty
528 $ hg ci -q -A -m 'add another file'
528 $ hg ci -q -A -m 'add another file'
529 $ hg status -A --rev 1:2 010a
529 $ hg status -A --rev 1:2 010a
530 C 010a
530 C 010a
531
531
532 $ cd ..
532 $ cd ..
533
533
534 test "hg status" with "directory pattern" which matches against files
534 test "hg status" with "directory pattern" which matches against files
535 only known on target revision.
535 only known on target revision.
536
536
537 $ hg init repo6
537 $ hg init repo6
538 $ cd repo6
538 $ cd repo6
539
539
540 $ echo a > a.txt
540 $ echo a > a.txt
541 $ hg add a.txt
541 $ hg add a.txt
542 $ hg commit -m '#0'
542 $ hg commit -m '#0'
543 $ mkdir -p 1/2/3/4/5
543 $ mkdir -p 1/2/3/4/5
544 $ echo b > 1/2/3/4/5/b.txt
544 $ echo b > 1/2/3/4/5/b.txt
545 $ hg add 1/2/3/4/5/b.txt
545 $ hg add 1/2/3/4/5/b.txt
546 $ hg commit -m '#1'
546 $ hg commit -m '#1'
547
547
548 $ hg update -C 0 > /dev/null
548 $ hg update -C 0 > /dev/null
549 $ hg status -A
549 $ hg status -A
550 C a.txt
550 C a.txt
551
551
552 the directory matching against specified pattern should be removed,
552 the directory matching against specified pattern should be removed,
553 because directory existence prevents 'dirstate.walk()' from showing
553 because directory existence prevents 'dirstate.walk()' from showing
554 warning message about such pattern.
554 warning message about such pattern.
555
555
556 $ test ! -d 1
556 $ test ! -d 1
557 $ hg status -A --rev 1 1/2/3/4/5/b.txt
557 $ hg status -A --rev 1 1/2/3/4/5/b.txt
558 R 1/2/3/4/5/b.txt
558 R 1/2/3/4/5/b.txt
559 $ hg status -A --rev 1 1/2/3/4/5
559 $ hg status -A --rev 1 1/2/3/4/5
560 R 1/2/3/4/5/b.txt
560 R 1/2/3/4/5/b.txt
561 $ hg status -A --rev 1 1/2/3
561 $ hg status -A --rev 1 1/2/3
562 R 1/2/3/4/5/b.txt
562 R 1/2/3/4/5/b.txt
563 $ hg status -A --rev 1 1
563 $ hg status -A --rev 1 1
564 R 1/2/3/4/5/b.txt
564 R 1/2/3/4/5/b.txt
565
565
566 $ hg status --config ui.formatdebug=True --rev 1 1
566 $ hg status --config ui.formatdebug=True --rev 1 1
567 status = [
567 status = [
568 {
568 {
569 'itemtype': 'file',
569 'itemtype': 'file',
570 'path': '1/2/3/4/5/b.txt',
570 'path': '1/2/3/4/5/b.txt',
571 'status': 'R'
571 'status': 'R'
572 },
572 },
573 ]
573 ]
574
574
575 #if windows
575 #if windows
576 $ hg --config ui.slash=false status -A --rev 1 1
576 $ hg --config ui.slash=false status -A --rev 1 1
577 R 1\2\3\4\5\b.txt
577 R 1\2\3\4\5\b.txt
578 #endif
578 #endif
579
579
580 $ cd ..
580 $ cd ..
581
581
582 Status after move overwriting a file (issue4458)
582 Status after move overwriting a file (issue4458)
583 =================================================
583 =================================================
584
584
585
585
586 $ hg init issue4458
586 $ hg init issue4458
587 $ cd issue4458
587 $ cd issue4458
588 $ echo a > a
588 $ echo a > a
589 $ echo b > b
589 $ echo b > b
590 $ hg commit -Am base
590 $ hg commit -Am base
591 adding a
591 adding a
592 adding b
592 adding b
593
593
594
594
595 with --force
595 with --force
596
596
597 $ hg mv b --force a
597 $ hg mv b --force a
598 $ hg st --copies
598 $ hg st --copies
599 M a
599 M a
600 b
600 b
601 R b
601 R b
602 $ hg revert --all
602 $ hg revert --all
603 reverting a
603 reverting a
604 undeleting b
604 undeleting b
605 $ rm *.orig
605 $ rm *.orig
606
606
607 without force
607 without force
608
608
609 $ hg rm a
609 $ hg rm a
610 $ hg st --copies
610 $ hg st --copies
611 R a
611 R a
612 $ hg mv b a
612 $ hg mv b a
613 $ hg st --copies
613 $ hg st --copies
614 M a
614 M a
615 b
615 b
616 R b
616 R b
617
617
618 using ui.statuscopies setting
618 using ui.statuscopies setting
619 $ hg st --config ui.statuscopies=true
619 $ hg st --config ui.statuscopies=true
620 M a
620 M a
621 b
621 b
622 R b
622 R b
623 $ hg st --config ui.statuscopies=false
623 $ hg st --config ui.statuscopies=false
624 M a
624 M a
625 R b
625 R b
626 $ hg st --config ui.tweakdefaults=yes
626 $ hg st --config ui.tweakdefaults=yes
627 M a
627 M a
628 b
628 b
629 R b
629 R b
630
630
631 using log status template (issue5155)
631 using log status template (issue5155)
632 $ hg log -Tstatus -r 'wdir()' -C
632 $ hg log -Tstatus -r 'wdir()' -C
633 changeset: 2147483647:ffffffffffff
633 changeset: 2147483647:ffffffffffff
634 parent: 0:8c55c58b4c0e
634 parent: 0:8c55c58b4c0e
635 user: test
635 user: test
636 date: * (glob)
636 date: * (glob)
637 files:
637 files:
638 M a
638 M a
639 b
639 b
640 R b
640 R b
641
641
642 $ hg log -GTstatus -r 'wdir()' -C
642 $ hg log -GTstatus -r 'wdir()' -C
643 o changeset: 2147483647:ffffffffffff
643 o changeset: 2147483647:ffffffffffff
644 | parent: 0:8c55c58b4c0e
644 | parent: 0:8c55c58b4c0e
645 ~ user: test
645 ~ user: test
646 date: * (glob)
646 date: * (glob)
647 files:
647 files:
648 M a
648 M a
649 b
649 b
650 R b
650 R b
651
651
652
652
653 Other "bug" highlight, the revision status does not report the copy information.
653 Other "bug" highlight, the revision status does not report the copy information.
654 This is buggy behavior.
654 This is buggy behavior.
655
655
656 $ hg commit -m 'blah'
656 $ hg commit -m 'blah'
657 $ hg st --copies --change .
657 $ hg st --copies --change .
658 M a
658 M a
659 R b
659 R b
660
660
661 using log status template, the copy information is displayed correctly.
661 using log status template, the copy information is displayed correctly.
662 $ hg log -Tstatus -r. -C
662 $ hg log -Tstatus -r. -C
663 changeset: 1:6685fde43d21
663 changeset: 1:6685fde43d21
664 tag: tip
664 tag: tip
665 user: test
665 user: test
666 date: * (glob)
666 date: * (glob)
667 summary: blah
667 summary: blah
668 files:
668 files:
669 M a
669 M a
670 b
670 b
671 R b
671 R b
672
672
673
673
674 $ cd ..
674 $ cd ..
675
675
676 Make sure .hg doesn't show up even as a symlink
676 Make sure .hg doesn't show up even as a symlink
677
677
678 $ hg init repo0
678 $ hg init repo0
679 $ mkdir symlink-repo0
679 $ mkdir symlink-repo0
680 $ cd symlink-repo0
680 $ cd symlink-repo0
681 $ ln -s ../repo0/.hg
681 $ ln -s ../repo0/.hg
682 $ hg status
682 $ hg status
683
683
684 Check using include flag with pattern when status does not need to traverse
684 Check using include flag with pattern when status does not need to traverse
685 the working directory (issue6483)
685 the working directory (issue6483)
686
686
687 $ cd ..
687 $ cd ..
688 $ hg init issue6483
688 $ hg init issue6483
689 $ cd issue6483
689 $ cd issue6483
690 $ touch a.py b.rs
690 $ touch a.py b.rs
691 $ hg add a.py b.rs
691 $ hg add a.py b.rs
692 $ hg st -aI "*.py"
692 $ hg st -aI "*.py"
693 A a.py
693 A a.py
694
694
695 Check using include flag while listing ignored composes correctly (issue6514)
695 Check using include flag while listing ignored composes correctly (issue6514)
696
696
697 $ cd ..
697 $ cd ..
698 $ hg init issue6514
698 $ hg init issue6514
699 $ cd issue6514
699 $ cd issue6514
700 $ mkdir ignored-folder
700 $ mkdir ignored-folder
701 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
701 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
702 $ cat >.hgignore <<EOF
702 $ cat >.hgignore <<EOF
703 > A.hs
703 > A.hs
704 > B.hs
704 > B.hs
705 > ignored-folder/
705 > ignored-folder/
706 > EOF
706 > EOF
707 $ hg st -i -I 're:.*\.hs$'
707 $ hg st -i -I 're:.*\.hs$'
708 I A.hs
708 I A.hs
709 I B.hs
709 I B.hs
710 I ignored-folder/ctest.hs
710 I ignored-folder/ctest.hs
711 I ignored-folder/other.txt (known-bad-output rust !)
General Comments 0
You need to be logged in to leave comments. Login now