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