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