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