##// END OF EJS Templates
rust-status: update rust-cpython bridge to account for the changes in core...
Raphaël Gomès -
r45016:f96b28aa default
parent child Browse files
Show More
@@ -1,756 +1,770
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 rayon::prelude::*;
29 29 use std::collections::VecDeque;
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,
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)]
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 impl ToString for BadType {
52 fn to_string(&self) -> String {
53 match self {
54 BadType::CharacterDevice => "character device",
55 BadType::BlockDevice => "block device",
56 BadType::FIFO => "fifo",
57 BadType::Socket => "socket",
58 BadType::Directory => "directory",
59 BadType::Unknown => "unknown",
60 }
61 .to_string()
62 }
63 }
64
51 65 /// Was explicitly matched but cannot be found/accessed
52 66 #[derive(Debug)]
53 67 pub enum BadMatch {
54 68 OsError(i32),
55 69 BadType(BadType),
56 70 }
57 71
58 72 /// Marker enum used to dispatch new status entries into the right collections.
59 73 /// Is similar to `crate::EntryState`, but represents the transient state of
60 74 /// entries during the lifetime of a command.
61 75 #[derive(Debug)]
62 76 enum Dispatch {
63 77 Unsure,
64 78 Modified,
65 79 Added,
66 80 Removed,
67 81 Deleted,
68 82 Clean,
69 83 Unknown,
70 84 Ignored,
71 85 /// Empty dispatch, the file is not worth listing
72 86 None,
73 87 /// Was explicitly matched but cannot be found/accessed
74 88 Bad(BadMatch),
75 89 Directory {
76 90 /// True if the directory used to be a file in the dmap so we can say
77 91 /// that it's been removed.
78 92 was_file: bool,
79 93 },
80 94 }
81 95
82 96 type IoResult<T> = std::io::Result<T>;
83 97
84 98 /// Dates and times that are outside the 31-bit signed range are compared
85 99 /// modulo 2^31. This should prevent hg from behaving badly with very large
86 100 /// files or corrupt dates while still having a high probability of detecting
87 101 /// changes. (issue2608)
88 102 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
89 103 /// is not defined for `i32`, and there is no `As` trait. This forces the
90 104 /// caller to cast `b` as `i32`.
91 105 fn mod_compare(a: i32, b: i32) -> bool {
92 106 a & i32::max_value() != b & i32::max_value()
93 107 }
94 108
95 109 /// Return a sorted list containing information about the entries
96 110 /// in the directory.
97 111 ///
98 112 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
99 113 fn list_directory(
100 114 path: impl AsRef<Path>,
101 115 skip_dot_hg: bool,
102 116 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
103 117 let mut results = vec![];
104 118 let entries = read_dir(path.as_ref())?;
105 119
106 120 for entry in entries {
107 121 let entry = entry?;
108 122 let filename = os_string_to_hg_path_buf(entry.file_name())?;
109 123 let file_type = entry.file_type()?;
110 124 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
111 125 return Ok(vec![]);
112 126 } else {
113 127 results.push((HgPathBuf::from(filename), entry))
114 128 }
115 129 }
116 130
117 131 results.sort_unstable_by_key(|e| e.0.clone());
118 132 Ok(results)
119 133 }
120 134
121 135 /// The file corresponding to the dirstate entry was found on the filesystem.
122 136 fn dispatch_found(
123 137 filename: impl AsRef<HgPath>,
124 138 entry: DirstateEntry,
125 139 metadata: HgMetadata,
126 140 copy_map: &CopyMap,
127 141 options: StatusOptions,
128 142 ) -> Dispatch {
129 143 let DirstateEntry {
130 144 state,
131 145 mode,
132 146 mtime,
133 147 size,
134 148 } = entry;
135 149
136 150 let HgMetadata {
137 151 st_mode,
138 152 st_size,
139 153 st_mtime,
140 154 ..
141 155 } = metadata;
142 156
143 157 match state {
144 158 EntryState::Normal => {
145 159 let size_changed = mod_compare(size, st_size as i32);
146 160 let mode_changed =
147 161 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
148 162 let metadata_changed = size >= 0 && (size_changed || mode_changed);
149 163 let other_parent = size == SIZE_FROM_OTHER_PARENT;
150 164 if metadata_changed
151 165 || other_parent
152 166 || copy_map.contains_key(filename.as_ref())
153 167 {
154 168 Dispatch::Modified
155 169 } else if mod_compare(mtime, st_mtime as i32) {
156 170 Dispatch::Unsure
157 171 } else if st_mtime == options.last_normal_time {
158 172 // the file may have just been marked as normal and
159 173 // it may have changed in the same second without
160 174 // changing its size. This can happen if we quickly
161 175 // do multiple commits. Force lookup, so we don't
162 176 // miss such a racy file change.
163 177 Dispatch::Unsure
164 178 } else if options.list_clean {
165 179 Dispatch::Clean
166 180 } else {
167 181 Dispatch::None
168 182 }
169 183 }
170 184 EntryState::Merged => Dispatch::Modified,
171 185 EntryState::Added => Dispatch::Added,
172 186 EntryState::Removed => Dispatch::Removed,
173 187 EntryState::Unknown => Dispatch::Unknown,
174 188 }
175 189 }
176 190
177 191 /// The file corresponding to this Dirstate entry is missing.
178 192 fn dispatch_missing(state: EntryState) -> Dispatch {
179 193 match state {
180 194 // File was removed from the filesystem during commands
181 195 EntryState::Normal | EntryState::Merged | EntryState::Added => {
182 196 Dispatch::Deleted
183 197 }
184 198 // File was removed, everything is normal
185 199 EntryState::Removed => Dispatch::Removed,
186 200 // File is unknown to Mercurial, everything is normal
187 201 EntryState::Unknown => Dispatch::Unknown,
188 202 }
189 203 }
190 204
191 205 lazy_static! {
192 206 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
193 207 let mut h = HashSet::new();
194 208 h.insert(HgPath::new(b""));
195 209 h
196 210 };
197 211 }
198 212
199 213 /// Get stat data about the files explicitly specified by match.
200 214 /// TODO subrepos
201 215 fn walk_explicit<'a>(
202 216 files: Option<&'a HashSet<&HgPath>>,
203 217 dmap: &'a DirstateMap,
204 218 root_dir: impl AsRef<Path> + Sync + Send + 'a,
205 219 options: StatusOptions,
206 220 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
207 221 files
208 222 .unwrap_or(&DEFAULT_WORK)
209 223 .par_iter()
210 224 .map(move |filename| {
211 225 // TODO normalization
212 226 let normalized = filename.as_ref();
213 227
214 228 let buf = match hg_path_to_path_buf(normalized) {
215 229 Ok(x) => x,
216 230 Err(e) => return Some(Err(e.into())),
217 231 };
218 232 let target = root_dir.as_ref().join(buf);
219 233 let st = target.symlink_metadata();
220 234 let in_dmap = dmap.get(normalized);
221 235 match st {
222 236 Ok(meta) => {
223 237 let file_type = meta.file_type();
224 238 return if file_type.is_file() || file_type.is_symlink() {
225 239 if let Some(entry) = in_dmap {
226 240 return Some(Ok((
227 241 normalized,
228 242 dispatch_found(
229 243 &normalized,
230 244 *entry,
231 245 HgMetadata::from_metadata(meta),
232 246 &dmap.copy_map,
233 247 options,
234 248 ),
235 249 )));
236 250 }
237 251 Some(Ok((normalized, Dispatch::Unknown)))
238 252 } else {
239 253 if file_type.is_dir() {
240 254 Some(Ok((
241 255 normalized,
242 256 Dispatch::Directory {
243 257 was_file: in_dmap.is_some(),
244 258 },
245 259 )))
246 260 } else {
247 261 Some(Ok((
248 262 normalized,
249 263 Dispatch::Bad(BadMatch::BadType(
250 264 // TODO do more than unknown
251 265 // Support for all `BadType` variant
252 266 // varies greatly between platforms.
253 267 // So far, no tests check the type and
254 268 // this should be good enough for most
255 269 // users.
256 270 BadType::Unknown,
257 271 )),
258 272 )))
259 273 }
260 274 };
261 275 }
262 276 Err(_) => {
263 277 if let Some(entry) = in_dmap {
264 278 return Some(Ok((
265 279 normalized,
266 280 dispatch_missing(entry.state),
267 281 )));
268 282 }
269 283 }
270 284 };
271 285 None
272 286 })
273 287 .flatten()
274 288 }
275 289
276 290 #[derive(Debug, Copy, Clone)]
277 291 pub struct StatusOptions {
278 292 /// Remember the most recent modification timeslot for status, to make
279 293 /// sure we won't miss future size-preserving file content modifications
280 294 /// that happen within the same timeslot.
281 295 pub last_normal_time: i64,
282 296 /// Whether we are on a filesystem with UNIX-like exec flags
283 297 pub check_exec: bool,
284 298 pub list_clean: bool,
285 299 pub list_unknown: bool,
286 300 pub list_ignored: bool,
287 301 }
288 302
289 303 /// Dispatch a single file found during `traverse`.
290 304 /// If `file` is a folder that needs to be traversed, it will be pushed into
291 305 /// `work`.
292 306 fn traverse_worker<'a>(
293 307 work: &mut VecDeque<HgPathBuf>,
294 308 matcher: &impl Matcher,
295 309 dmap: &DirstateMap,
296 310 filename: impl AsRef<HgPath>,
297 311 dir_entry: &DirEntry,
298 312 ignore_fn: &impl for<'r> Fn(&'r HgPath) -> bool,
299 313 dir_ignore_fn: &impl for<'r> Fn(&'r HgPath) -> bool,
300 314 options: StatusOptions,
301 315 ) -> Option<IoResult<(Cow<'a, HgPath>, Dispatch)>> {
302 316 let file_type = match dir_entry.file_type() {
303 317 Ok(x) => x,
304 318 Err(e) => return Some(Err(e.into())),
305 319 };
306 320 let filename = filename.as_ref();
307 321 let entry_option = dmap.get(filename);
308 322
309 323 if file_type.is_dir() {
310 324 // Do we need to traverse it?
311 325 if !ignore_fn(&filename) || options.list_ignored {
312 326 work.push_front(filename.to_owned());
313 327 }
314 328 // Nested `if` until `rust-lang/rust#53668` is stable
315 329 if let Some(entry) = entry_option {
316 330 // Used to be a file, is now a folder
317 331 if matcher.matches_everything() || matcher.matches(&filename) {
318 332 return Some(Ok((
319 333 Cow::Owned(filename.to_owned()),
320 334 dispatch_missing(entry.state),
321 335 )));
322 336 }
323 337 }
324 338 } else if file_type.is_file() || file_type.is_symlink() {
325 339 if let Some(entry) = entry_option {
326 340 if matcher.matches_everything() || matcher.matches(&filename) {
327 341 let metadata = match dir_entry.metadata() {
328 342 Ok(x) => x,
329 343 Err(e) => return Some(Err(e.into())),
330 344 };
331 345 return Some(Ok((
332 346 Cow::Owned(filename.to_owned()),
333 347 dispatch_found(
334 348 &filename,
335 349 *entry,
336 350 HgMetadata::from_metadata(metadata),
337 351 &dmap.copy_map,
338 352 options,
339 353 ),
340 354 )));
341 355 }
342 356 } else if (matcher.matches_everything() || matcher.matches(&filename))
343 357 && !ignore_fn(&filename)
344 358 {
345 359 if (options.list_ignored || matcher.exact_match(&filename))
346 360 && dir_ignore_fn(&filename)
347 361 {
348 362 if options.list_ignored {
349 363 return Some(Ok((
350 364 Cow::Owned(filename.to_owned()),
351 365 Dispatch::Ignored,
352 366 )));
353 367 }
354 368 } else {
355 369 return Some(Ok((
356 370 Cow::Owned(filename.to_owned()),
357 371 Dispatch::Unknown,
358 372 )));
359 373 }
360 374 }
361 375 } else if let Some(entry) = entry_option {
362 376 // Used to be a file or a folder, now something else.
363 377 if matcher.matches_everything() || matcher.matches(&filename) {
364 378 return Some(Ok((
365 379 Cow::Owned(filename.to_owned()),
366 380 dispatch_missing(entry.state),
367 381 )));
368 382 }
369 383 }
370 384 None
371 385 }
372 386
373 387 /// Walk the working directory recursively to look for changes compared to the
374 388 /// current `DirstateMap`.
375 389 fn traverse<'a>(
376 390 matcher: &(impl Matcher + Sync),
377 391 root_dir: impl AsRef<Path>,
378 392 dmap: &DirstateMap,
379 393 path: impl AsRef<HgPath>,
380 394 old_results: FastHashMap<Cow<'a, HgPath>, Dispatch>,
381 395 ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync),
382 396 dir_ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync),
383 397 options: StatusOptions,
384 398 ) -> IoResult<FastHashMap<Cow<'a, HgPath>, Dispatch>> {
385 399 let root_dir = root_dir.as_ref();
386 400 let mut new_results = FastHashMap::default();
387 401
388 402 let mut work = VecDeque::new();
389 403 work.push_front(path.as_ref().to_owned());
390 404
391 405 while let Some(ref directory) = work.pop_front() {
392 406 if directory.as_bytes() == b".hg" {
393 407 continue;
394 408 }
395 409 let visit_entries = match matcher.visit_children_set(directory) {
396 410 VisitChildrenSet::Empty => continue,
397 411 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
398 412 VisitChildrenSet::Set(set) => Some(set),
399 413 };
400 414 let buf = hg_path_to_path_buf(directory)?;
401 415 let dir_path = root_dir.join(buf);
402 416
403 417 let skip_dot_hg = !directory.as_bytes().is_empty();
404 418 let entries = match list_directory(dir_path, skip_dot_hg) {
405 419 Err(e) => match e.kind() {
406 420 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
407 421 new_results.insert(
408 422 Cow::Owned(directory.to_owned()),
409 423 Dispatch::Bad(BadMatch::OsError(
410 424 // Unwrapping here is OK because the error always
411 425 // is a real os error
412 426 e.raw_os_error().unwrap(),
413 427 )),
414 428 );
415 429 continue;
416 430 }
417 431 _ => return Err(e),
418 432 },
419 433 Ok(entries) => entries,
420 434 };
421 435
422 436 for (filename, dir_entry) in entries {
423 437 if let Some(ref set) = visit_entries {
424 438 if !set.contains(filename.deref()) {
425 439 continue;
426 440 }
427 441 }
428 442 // TODO normalize
429 443 let filename = if directory.is_empty() {
430 444 filename.to_owned()
431 445 } else {
432 446 directory.join(&filename)
433 447 };
434 448
435 449 if !old_results.contains_key(filename.deref()) {
436 450 if let Some((res, dispatch)) = traverse_worker(
437 451 &mut work,
438 452 matcher,
439 453 &dmap,
440 454 &filename,
441 455 &dir_entry,
442 456 &ignore_fn,
443 457 &dir_ignore_fn,
444 458 options,
445 459 )
446 460 .transpose()?
447 461 {
448 462 new_results.insert(res, dispatch);
449 463 }
450 464 }
451 465 }
452 466 }
453 467
454 468 new_results.extend(old_results.into_iter());
455 469
456 470 Ok(new_results)
457 471 }
458 472
459 473 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
460 474 fn stat_dmap_entries(
461 475 dmap: &DirstateMap,
462 476 root_dir: impl AsRef<Path> + Sync + Send,
463 477 options: StatusOptions,
464 478 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
465 479 dmap.par_iter().map(move |(filename, entry)| {
466 480 let filename: &HgPath = filename;
467 481 let filename_as_path = hg_path_to_path_buf(filename)?;
468 482 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
469 483
470 484 match meta {
471 485 Ok(ref m)
472 486 if !(m.file_type().is_file()
473 487 || m.file_type().is_symlink()) =>
474 488 {
475 489 Ok((filename, dispatch_missing(entry.state)))
476 490 }
477 491 Ok(m) => Ok((
478 492 filename,
479 493 dispatch_found(
480 494 filename,
481 495 *entry,
482 496 HgMetadata::from_metadata(m),
483 497 &dmap.copy_map,
484 498 options,
485 499 ),
486 500 )),
487 501 Err(ref e)
488 502 if e.kind() == ErrorKind::NotFound
489 503 || e.raw_os_error() == Some(20) =>
490 504 {
491 505 // Rust does not yet have an `ErrorKind` for
492 506 // `NotADirectory` (errno 20)
493 507 // It happens if the dirstate contains `foo/bar` and
494 508 // foo is not a directory
495 509 Ok((filename, dispatch_missing(entry.state)))
496 510 }
497 511 Err(e) => Err(e),
498 512 }
499 513 })
500 514 }
501 515
502 516 pub struct DirstateStatus<'a> {
503 517 pub modified: Vec<Cow<'a, HgPath>>,
504 518 pub added: Vec<Cow<'a, HgPath>>,
505 519 pub removed: Vec<Cow<'a, HgPath>>,
506 520 pub deleted: Vec<Cow<'a, HgPath>>,
507 521 pub clean: Vec<Cow<'a, HgPath>>,
508 522 pub ignored: Vec<Cow<'a, HgPath>>,
509 523 pub unknown: Vec<Cow<'a, HgPath>>,
510 524 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
511 525 }
512 526
513 527 fn build_response<'a>(
514 528 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
515 529 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
516 530 let mut lookup = vec![];
517 531 let mut modified = vec![];
518 532 let mut added = vec![];
519 533 let mut removed = vec![];
520 534 let mut deleted = vec![];
521 535 let mut clean = vec![];
522 536 let mut ignored = vec![];
523 537 let mut unknown = vec![];
524 538 let mut bad = vec![];
525 539
526 540 for (filename, dispatch) in results.into_iter() {
527 541 match dispatch {
528 542 Dispatch::Unknown => unknown.push(filename),
529 543 Dispatch::Unsure => lookup.push(filename),
530 544 Dispatch::Modified => modified.push(filename),
531 545 Dispatch::Added => added.push(filename),
532 546 Dispatch::Removed => removed.push(filename),
533 547 Dispatch::Deleted => deleted.push(filename),
534 548 Dispatch::Clean => clean.push(filename),
535 549 Dispatch::Ignored => ignored.push(filename),
536 550 Dispatch::None => {}
537 551 Dispatch::Bad(reason) => bad.push((filename, reason)),
538 552 Dispatch::Directory { .. } => {}
539 553 }
540 554 }
541 555
542 556 (
543 557 lookup,
544 558 DirstateStatus {
545 559 modified,
546 560 added,
547 561 removed,
548 562 deleted,
549 563 clean,
550 564 ignored,
551 565 unknown,
552 566 bad,
553 567 },
554 568 )
555 569 }
556 570
557 571 pub enum StatusError {
558 572 IO(std::io::Error),
559 573 Path(HgPathError),
560 574 Pattern(PatternError),
561 575 }
562 576
563 577 pub type StatusResult<T> = Result<T, StatusError>;
564 578
565 579 impl From<PatternError> for StatusError {
566 580 fn from(e: PatternError) -> Self {
567 581 StatusError::Pattern(e)
568 582 }
569 583 }
570 584 impl From<HgPathError> for StatusError {
571 585 fn from(e: HgPathError) -> Self {
572 586 StatusError::Path(e)
573 587 }
574 588 }
575 589 impl From<std::io::Error> for StatusError {
576 590 fn from(e: std::io::Error) -> Self {
577 591 StatusError::IO(e)
578 592 }
579 593 }
580 594
581 595 impl ToString for StatusError {
582 596 fn to_string(&self) -> String {
583 597 match self {
584 598 StatusError::IO(e) => e.to_string(),
585 599 StatusError::Path(e) => e.to_string(),
586 600 StatusError::Pattern(e) => e.to_string(),
587 601 }
588 602 }
589 603 }
590 604
591 605 /// Get the status of files in the working directory.
592 606 ///
593 607 /// This is the current entry-point for `hg-core` and is realistically unusable
594 608 /// outside of a Python context because its arguments need to provide a lot of
595 609 /// information that will not be necessary in the future.
596 610 pub fn status<'a: 'c, 'b: 'c, 'c>(
597 611 dmap: &'a DirstateMap,
598 612 matcher: &'b (impl Matcher + Sync),
599 613 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
600 614 ignore_files: &[impl AsRef<Path> + 'c],
601 615 options: StatusOptions,
602 616 ) -> StatusResult<(
603 617 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
604 618 Vec<PatternFileWarning>,
605 619 )> {
606 620 let (ignore_fn, warnings) = get_ignore_function(&ignore_files, root_dir)?;
607 621
608 622 // Is the path or one of its ancestors ignored?
609 623 let dir_ignore_fn = |dir: &_| {
610 624 if ignore_fn(dir) {
611 625 true
612 626 } else {
613 627 for p in find_dirs(dir) {
614 628 if ignore_fn(p) {
615 629 return true;
616 630 }
617 631 }
618 632 false
619 633 }
620 634 };
621 635
622 636 let files = matcher.file_set();
623 637
624 638 // Step 1: check the files explicitly mentioned by the user
625 639 let explicit = walk_explicit(files, &dmap, root_dir, options);
626 640 let (work, mut results): (Vec<_>, FastHashMap<_, _>) = explicit
627 641 .filter_map(Result::ok)
628 642 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
629 643 .partition(|(_, dispatch)| match dispatch {
630 644 Dispatch::Directory { .. } => true,
631 645 _ => false,
632 646 });
633 647
634 648 // Step 2: recursively check the working directory for changes if needed
635 649 for (dir, dispatch) in work {
636 650 match dispatch {
637 651 Dispatch::Directory { was_file } => {
638 652 if was_file {
639 653 results.insert(dir.to_owned(), Dispatch::Removed);
640 654 }
641 655 if options.list_ignored
642 656 || options.list_unknown && !dir_ignore_fn(&dir)
643 657 {
644 658 results = traverse(
645 659 matcher,
646 660 root_dir,
647 661 &dmap,
648 662 &dir,
649 663 results,
650 664 &ignore_fn,
651 665 &dir_ignore_fn,
652 666 options,
653 667 )?;
654 668 }
655 669 }
656 670 _ => unreachable!("There can only be directories in `work`"),
657 671 }
658 672 }
659 673
660 674 if !matcher.is_exact() {
661 675 // Step 3: Check the remaining files from the dmap.
662 676 // If a dmap file is not in results yet, it was either
663 677 // a) not matched b) ignored, c) missing, or d) under a
664 678 // symlink directory.
665 679
666 680 if options.list_unknown {
667 681 let to_visit: Box<dyn Iterator<Item = (&HgPath, &DirstateEntry)>> =
668 682 if results.is_empty() && matcher.matches_everything() {
669 683 Box::new(dmap.iter().map(|(f, e)| (f.deref(), e)))
670 684 } else {
671 685 Box::new(dmap.iter().filter_map(|(f, e)| {
672 686 if !results.contains_key(f.deref())
673 687 && matcher.matches(f)
674 688 {
675 689 Some((f.deref(), e))
676 690 } else {
677 691 None
678 692 }
679 693 }))
680 694 };
681 695 let mut to_visit: Vec<_> = to_visit.collect();
682 696 to_visit.sort_by(|a, b| a.0.cmp(&b.0));
683 697
684 698 // We walked all dirs under the roots that weren't ignored, and
685 699 // everything that matched was stat'ed and is already in results.
686 700 // The rest must thus be ignored or under a symlink.
687 701 let mut path_auditor = PathAuditor::new(root_dir);
688 702
689 703 for (ref filename, entry) in to_visit {
690 704 // Report ignored items in the dmap as long as they are not
691 705 // under a symlink directory.
692 706 if path_auditor.check(filename) {
693 707 // TODO normalize for case-insensitive filesystems
694 708 let buf = hg_path_to_path_buf(filename)?;
695 709 results.insert(
696 710 Cow::Borrowed(filename),
697 711 match root_dir.as_ref().join(&buf).symlink_metadata() {
698 712 // File was just ignored, no links, and exists
699 713 Ok(meta) => {
700 714 let metadata = HgMetadata::from_metadata(meta);
701 715 dispatch_found(
702 716 filename,
703 717 *entry,
704 718 metadata,
705 719 &dmap.copy_map,
706 720 options,
707 721 )
708 722 }
709 723 // File doesn't exist
710 724 Err(_) => dispatch_missing(entry.state),
711 725 },
712 726 );
713 727 } else {
714 728 // It's either missing or under a symlink directory which
715 729 // we, in this case, report as missing.
716 730 results.insert(
717 731 Cow::Borrowed(filename),
718 732 dispatch_missing(entry.state),
719 733 );
720 734 }
721 735 }
722 736 } else {
723 737 // We may not have walked the full directory tree above, so stat
724 738 // and check everything we missed.
725 739 let stat_results = stat_dmap_entries(&dmap, root_dir, options);
726 740 results.par_extend(stat_results.flatten().map(
727 741 |(filename, dispatch)| (Cow::Borrowed(filename), dispatch),
728 742 ));
729 743 }
730 744 }
731 745
732 746 let results = results.into_iter().filter_map(|(filename, dispatch)| {
733 747 match dispatch {
734 748 Dispatch::Bad(_) => return Some((filename, dispatch)),
735 749 _ => {}
736 750 };
737 751 // TODO do this in //, not at the end
738 752 if !dmap.contains_key(filename.deref()) {
739 753 if (options.list_ignored || matcher.exact_match(&filename))
740 754 && dir_ignore_fn(&filename)
741 755 {
742 756 if options.list_ignored {
743 757 return Some((filename.to_owned(), Dispatch::Ignored));
744 758 }
745 759 } else {
746 760 if !ignore_fn(&filename) {
747 761 return Some((filename.to_owned(), Dispatch::Unknown));
748 762 }
749 763 }
750 764 return None;
751 765 }
752 766 Some((filename, dispatch))
753 767 });
754 768
755 769 Ok((build_response(results), warnings))
756 770 }
@@ -1,133 +1,144
1 1 // dirstate.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::dirstate` module provided by the
9 9 //! `hg-core` package.
10 10 //!
11 11 //! From Python, this will be seen as `mercurial.rustext.dirstate`
12 12 mod copymap;
13 13 mod dirs_multiset;
14 14 mod dirstate_map;
15 15 mod non_normal_entries;
16 16 mod status;
17 use crate::dirstate::{
18 dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper,
17 use crate::{
18 dirstate::{
19 dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper,
20 },
21 exceptions,
19 22 };
20 23 use cpython::{
21 exc, PyBytes, PyDict, PyErr, PyModule, PyObject, PyResult, PySequence,
22 Python,
24 exc, PyBytes, PyDict, PyErr, PyList, PyModule, PyObject, PyResult,
25 PySequence, Python,
23 26 };
24 27 use hg::{
25 28 utils::hg_path::HgPathBuf, DirstateEntry, DirstateParseError, EntryState,
26 29 StateMap,
27 30 };
28 31 use libc::{c_char, c_int};
29 32 use std::convert::TryFrom;
30 33
31 34 // C code uses a custom `dirstate_tuple` type, checks in multiple instances
32 35 // for this type, and raises a Python `Exception` if the check does not pass.
33 36 // Because this type differs only in name from the regular Python tuple, it
34 37 // would be a good idea in the near future to remove it entirely to allow
35 38 // for a pure Python tuple of the same effective structure to be used,
36 39 // rendering this type and the capsule below useless.
37 40 py_capsule_fn!(
38 41 from mercurial.cext.parsers import make_dirstate_tuple_CAPI
39 42 as make_dirstate_tuple_capi
40 43 signature (
41 44 state: c_char,
42 45 mode: c_int,
43 46 size: c_int,
44 47 mtime: c_int,
45 48 ) -> *mut RawPyObject
46 49 );
47 50
48 51 pub fn make_dirstate_tuple(
49 52 py: Python,
50 53 entry: &DirstateEntry,
51 54 ) -> PyResult<PyObject> {
52 55 // might be silly to retrieve capsule function in hot loop
53 56 let make = make_dirstate_tuple_capi::retrieve(py)?;
54 57
55 58 let &DirstateEntry {
56 59 state,
57 60 mode,
58 61 size,
59 62 mtime,
60 63 } = entry;
61 64 // Explicitly go through u8 first, then cast to platform-specific `c_char`
62 65 // because Into<u8> has a specific implementation while `as c_char` would
63 66 // just do a naive enum cast.
64 67 let state_code: u8 = state.into();
65 68
66 69 let maybe_obj = unsafe {
67 70 let ptr = make(state_code as c_char, mode, size, mtime);
68 71 PyObject::from_owned_ptr_opt(py, ptr)
69 72 };
70 73 maybe_obj.ok_or_else(|| PyErr::fetch(py))
71 74 }
72 75
73 76 pub fn extract_dirstate(py: Python, dmap: &PyDict) -> Result<StateMap, PyErr> {
74 77 dmap.items(py)
75 78 .iter()
76 79 .map(|(filename, stats)| {
77 80 let stats = stats.extract::<PySequence>(py)?;
78 81 let state = stats.get_item(py, 0)?.extract::<PyBytes>(py)?;
79 82 let state = EntryState::try_from(state.data(py)[0]).map_err(
80 83 |e: DirstateParseError| {
81 84 PyErr::new::<exc::ValueError, _>(py, e.to_string())
82 85 },
83 86 )?;
84 87 let mode = stats.get_item(py, 1)?.extract(py)?;
85 88 let size = stats.get_item(py, 2)?.extract(py)?;
86 89 let mtime = stats.get_item(py, 3)?.extract(py)?;
87 90 let filename = filename.extract::<PyBytes>(py)?;
88 91 let filename = filename.data(py);
89 92 Ok((
90 93 HgPathBuf::from(filename.to_owned()),
91 94 DirstateEntry {
92 95 state,
93 96 mode,
94 97 size,
95 98 mtime,
96 99 },
97 100 ))
98 101 })
99 102 .collect()
100 103 }
101 104
102 105 /// Create the module, with `__package__` given from parent
103 106 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
104 107 let dotted_name = &format!("{}.dirstate", package);
105 108 let m = PyModule::new(py, dotted_name)?;
106 109
107 110 m.add(py, "__package__", package)?;
108 111 m.add(py, "__doc__", "Dirstate - Rust implementation")?;
109 112
113 m.add(
114 py,
115 "FallbackError",
116 py.get_type::<exceptions::FallbackError>(),
117 )?;
110 118 m.add_class::<Dirs>(py)?;
111 119 m.add_class::<DirstateMap>(py)?;
112 120 m.add(
113 121 py,
114 122 "status",
115 123 py_fn!(
116 124 py,
117 125 status_wrapper(
118 126 dmap: DirstateMap,
119 127 root_dir: PyObject,
120 128 matcher: PyObject,
121 list_clean: bool,
129 ignorefiles: PyList,
130 check_exec: bool,
122 131 last_normal_time: i64,
123 check_exec: bool
132 list_clean: bool,
133 list_ignored: bool,
134 list_unknown: bool
124 135 )
125 136 ),
126 137 )?;
127 138
128 139 let sys = PyModule::import(py, "sys")?;
129 140 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
130 141 sys_modules.set_item(py, dotted_name, &m)?;
131 142
132 143 Ok(m)
133 144 }
@@ -1,129 +1,294
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 use crate::dirstate::DirstateMap;
13 use cpython::exc::ValueError;
12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
14 13 use cpython::{
15 ObjectProtocol, PyBytes, PyErr, PyList, PyObject, PyResult, PyTuple,
16 Python, PythonObject, ToPyObject,
14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
17 16 };
18 use hg::utils::hg_path::HgPathBuf;
19 17 use hg::{
20 matchers::{AlwaysMatcher, FileMatcher},
21 status,
22 utils::{files::get_path_from_bytes, hg_path::HgPath},
23 DirstateStatus,
18 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
19 parse_pattern_syntax, status,
20 utils::{
21 files::{get_bytes_from_path, get_path_from_bytes},
22 hg_path::{HgPath, HgPathBuf},
23 },
24 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
25 StatusOptions,
24 26 };
25 use std::borrow::Borrow;
27 use std::borrow::{Borrow, Cow};
26 28
27 29 /// This will be useless once trait impls for collection are added to `PyBytes`
28 30 /// upstream.
29 fn collect_pybytes_list<P: AsRef<HgPath>>(
31 fn collect_pybytes_list(
30 32 py: Python,
31 collection: &[P],
33 collection: &[impl AsRef<HgPath>],
32 34 ) -> PyList {
33 35 let list = PyList::new(py, &[]);
34 36
35 for (i, path) in collection.iter().enumerate() {
36 list.insert(
37 for path in collection.iter() {
38 list.append(
37 39 py,
38 i,
39 40 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
40 41 )
41 42 }
42 43
43 44 list
44 45 }
45 46
47 fn collect_bad_matches(
48 py: Python,
49 collection: &[(impl AsRef<HgPath>, BadMatch)],
50 ) -> PyResult<PyList> {
51 let list = PyList::new(py, &[]);
52
53 let os = py.import("os")?;
54 let get_error_message = |code: i32| -> PyResult<_> {
55 os.call(
56 py,
57 "strerror",
58 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
59 None,
60 )
61 };
62
63 for (path, bad_match) in collection.iter() {
64 let message = match bad_match {
65 BadMatch::OsError(code) => get_error_message(*code)?,
66 BadMatch::BadType(bad_type) => format!(
67 "unsupported file type (type is {})",
68 bad_type.to_string()
69 )
70 .to_py_object(py)
71 .into_object(),
72 };
73 list.append(
74 py,
75 (PyBytes::new(py, path.as_ref().as_bytes()), message)
76 .to_py_object(py)
77 .into_object(),
78 )
79 }
80
81 Ok(list)
82 }
83
84 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
85 match err {
86 StatusError::Pattern(e) => {
87 PyErr::new::<FallbackError, _>(py, e.to_string())
88 }
89 e => PyErr::new::<ValueError, _>(py, e.to_string()),
90 }
91 }
92
46 93 pub fn status_wrapper(
47 94 py: Python,
48 95 dmap: DirstateMap,
49 96 matcher: PyObject,
50 97 root_dir: PyObject,
51 list_clean: bool,
98 ignore_files: PyList,
99 check_exec: bool,
52 100 last_normal_time: i64,
53 check_exec: bool,
54 ) -> PyResult<(PyList, PyList, PyList, PyList, PyList, PyList, PyList)> {
101 list_clean: bool,
102 list_ignored: bool,
103 list_unknown: bool,
104 ) -> PyResult<PyTuple> {
55 105 let bytes = root_dir.extract::<PyBytes>(py)?;
56 106 let root_dir = get_path_from_bytes(bytes.data(py));
57 107
58 108 let dmap: DirstateMap = dmap.to_py_object(py);
59 109 let dmap = dmap.get_inner(py);
60 110
111 let ignore_files: PyResult<Vec<_>> = ignore_files
112 .iter(py)
113 .map(|b| {
114 let file = b.extract::<PyBytes>(py)?;
115 Ok(get_path_from_bytes(file.data(py)).to_owned())
116 })
117 .collect();
118 let ignore_files = ignore_files?;
119
61 120 match matcher.get_type(py).name(py).borrow() {
62 121 "alwaysmatcher" => {
63 122 let matcher = AlwaysMatcher;
64 let (lookup, status_res) = status(
123 let ((lookup, status_res), warnings) = status(
65 124 &dmap,
66 125 &matcher,
67 126 &root_dir,
68 list_clean,
69 last_normal_time,
70 check_exec,
127 &ignore_files,
128 StatusOptions {
129 check_exec,
130 last_normal_time,
131 list_clean,
132 list_ignored,
133 list_unknown,
134 },
71 135 )
72 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
73 build_response(lookup, status_res, py)
136 .map_err(|e| handle_fallback(py, e))?;
137 build_response(py, lookup, status_res, warnings)
74 138 }
75 139 "exactmatcher" => {
76 140 let files = matcher.call_method(
77 141 py,
78 142 "files",
79 143 PyTuple::new(py, &[]),
80 144 None,
81 145 )?;
82 146 let files: PyList = files.cast_into(py)?;
83 147 let files: PyResult<Vec<HgPathBuf>> = files
84 148 .iter(py)
85 149 .map(|f| {
86 150 Ok(HgPathBuf::from_bytes(
87 151 f.extract::<PyBytes>(py)?.data(py),
88 152 ))
89 153 })
90 154 .collect();
91 155
92 156 let files = files?;
93 157 let matcher = FileMatcher::new(&files)
94 158 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
95 let (lookup, status_res) = status(
159 let ((lookup, status_res), warnings) = status(
96 160 &dmap,
97 161 &matcher,
98 162 &root_dir,
99 list_clean,
100 last_normal_time,
101 check_exec,
163 &ignore_files,
164 StatusOptions {
165 check_exec,
166 last_normal_time,
167 list_clean,
168 list_ignored,
169 list_unknown,
170 },
102 171 )
103 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
104 build_response(lookup, status_res, py)
172 .map_err(|e| handle_fallback(py, e))?;
173 build_response(py, lookup, status_res, warnings)
174 }
175 "includematcher" => {
176 // Get the patterns from Python even though most of them are
177 // redundant with those we will parse later on, as they include
178 // those passed from the command line.
179 let ignore_patterns: PyResult<Vec<_>> = matcher
180 .getattr(py, "_kindpats")?
181 .iter(py)?
182 .map(|k| {
183 let k = k?;
184 let syntax = parse_pattern_syntax(
185 &[
186 k.get_item(py, 0)?
187 .extract::<PyBytes>(py)?
188 .data(py),
189 &b":"[..],
190 ]
191 .concat(),
192 )
193 .map_err(|e| {
194 handle_fallback(py, StatusError::Pattern(e))
195 })?;
196 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
197 let pattern = pattern.data(py);
198 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
199 let source = get_path_from_bytes(source.data(py));
200 let new = IgnorePattern::new(syntax, pattern, source);
201 Ok(new)
202 })
203 .collect();
204
205 let ignore_patterns = ignore_patterns?;
206 let mut all_warnings = vec![];
207
208 let (matcher, warnings) =
209 IncludeMatcher::new(ignore_patterns, &root_dir)
210 .map_err(|e| handle_fallback(py, e.into()))?;
211 all_warnings.extend(warnings);
212
213 let ((lookup, status_res), warnings) = status(
214 &dmap,
215 &matcher,
216 &root_dir,
217 &ignore_files,
218 StatusOptions {
219 check_exec,
220 last_normal_time,
221 list_clean,
222 list_ignored,
223 list_unknown,
224 },
225 )
226 .map_err(|e| handle_fallback(py, e))?;
227
228 all_warnings.extend(warnings);
229
230 build_response(py, lookup, status_res, all_warnings)
105 231 }
106 232 e => {
107 233 return Err(PyErr::new::<ValueError, _>(
108 234 py,
109 235 format!("Unsupported matcher {}", e),
110 236 ));
111 237 }
112 238 }
113 239 }
114 240
115 241 fn build_response(
116 lookup: Vec<&HgPath>,
242 py: Python,
243 lookup: Vec<Cow<HgPath>>,
117 244 status_res: DirstateStatus,
118 py: Python,
119 ) -> PyResult<(PyList, PyList, PyList, PyList, PyList, PyList, PyList)> {
245 warnings: Vec<PatternFileWarning>,
246 ) -> PyResult<PyTuple> {
120 247 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
121 248 let added = collect_pybytes_list(py, status_res.added.as_ref());
122 249 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
123 250 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
124 251 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
252 let ignored = collect_pybytes_list(py, status_res.ignored.as_ref());
253 let unknown = collect_pybytes_list(py, status_res.unknown.as_ref());
125 254 let lookup = collect_pybytes_list(py, lookup.as_ref());
126 let unknown = PyList::new(py, &[]);
255 let bad = collect_bad_matches(py, status_res.bad.as_ref())?;
256 let py_warnings = PyList::new(py, &[]);
257 for warning in warnings.iter() {
258 // We use duck-typing on the Python side for dispatch, good enough for
259 // now.
260 match warning {
261 PatternFileWarning::InvalidSyntax(file, syn) => {
262 py_warnings.append(
263 py,
264 (
265 PyBytes::new(py, &get_bytes_from_path(&file)),
266 PyBytes::new(py, syn),
267 )
268 .to_py_object(py)
269 .into_object(),
270 );
271 }
272 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
273 py,
274 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
275 ),
276 }
277 }
127 278
128 Ok((lookup, modified, added, removed, deleted, unknown, clean))
279 Ok(PyTuple::new(
280 py,
281 &[
282 lookup.into_object(),
283 modified.into_object(),
284 added.into_object(),
285 removed.into_object(),
286 deleted.into_object(),
287 clean.into_object(),
288 ignored.into_object(),
289 unknown.into_object(),
290 py_warnings.into_object(),
291 bad.into_object(),
292 ][..],
293 ))
129 294 }
@@ -1,42 +1,44
1 1 // ancestors.rs
2 2 //
3 3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
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 Rust errors
9 9 //!
10 10 //! [`GraphError`] exposes `hg::GraphError` as a subclass of `ValueError`
11 11 //! but some variants of `hg::GraphError` can be converted directly to other
12 12 //! existing Python exceptions if appropriate.
13 13 //!
14 14 //! [`GraphError`]: struct.GraphError.html
15 15 use cpython::{
16 16 exc::{RuntimeError, ValueError},
17 17 py_exception, PyErr, Python,
18 18 };
19 19 use hg;
20 20
21 21 py_exception!(rustext, GraphError, ValueError);
22 22
23 23 impl GraphError {
24 24 pub fn pynew(py: Python, inner: hg::GraphError) -> PyErr {
25 25 match inner {
26 26 hg::GraphError::ParentOutOfRange(r) => {
27 27 GraphError::new(py, ("ParentOutOfRange", r))
28 28 }
29 29 hg::GraphError::WorkingDirectoryUnsupported => {
30 30 match py
31 31 .import("mercurial.error")
32 32 .and_then(|m| m.get(py, "WdirUnsupported"))
33 33 {
34 34 Err(e) => e,
35 35 Ok(cls) => PyErr::from_instance(py, cls),
36 36 }
37 37 }
38 38 }
39 39 }
40 40 }
41 41
42 42 py_exception!(rustext, HgPathPyError, RuntimeError);
43 py_exception!(rustext, FallbackError, RuntimeError);
44 py_exception!(shared_ref, AlreadyBorrowed, RuntimeError);
General Comments 0
You need to be logged in to leave comments. Login now