status.rs
942 lines
| 32.9 KiB
| application/rls-services+xml
|
RustLexer
|
r43565 | // status.rs | ||
// | ||||
// Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | ||||
// | ||||
// This software may be used and distributed according to the terms of the | ||||
// GNU General Public License version 2 or any later version. | ||||
//! Rust implementation of dirstate.status (dirstate.py). | ||||
//! It is currently missing a lot of functionality compared to the Python one | ||||
//! and will only be triggered in narrow cases. | ||||
|
r48126 | use crate::dirstate_tree::on_disk::DirstateV2ParseError; | ||
|
r46185 | use crate::utils::path_auditor::PathAuditor; | ||
|
r44003 | use crate::{ | ||
dirstate::SIZE_FROM_OTHER_PARENT, | ||||
|
r45015 | filepatterns::PatternFileWarning, | ||
matchers::{get_ignore_function, Matcher, VisitChildrenSet}, | ||||
|
r44003 | utils::{ | ||
|
r45015 | files::{find_dirs, HgMetadata}, | ||
|
r45010 | hg_path::{ | ||
hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf, | ||||
|
r45015 | HgPathError, | ||
|
r45010 | }, | ||
|
r44003 | }, | ||
|
r45014 | CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap, | ||
|
r45015 | PatternError, | ||
|
r44003 | }; | ||
|
r45015 | use lazy_static::lazy_static; | ||
|
r45028 | use micro_timer::timed; | ||
|
r43565 | use rayon::prelude::*; | ||
|
r45015 | use std::{ | ||
borrow::Cow, | ||||
collections::HashSet, | ||||
|
r47173 | fmt, | ||
|
r45015 | fs::{read_dir, DirEntry}, | ||
io::ErrorKind, | ||||
ops::Deref, | ||||
|
r45088 | path::{Path, PathBuf}, | ||
|
r45015 | }; | ||
|
r43565 | |||
|
r45013 | /// Wrong type of file from a `BadMatch` | ||
/// Note: a lot of those don't exist on all platforms. | ||||
|
r45023 | #[derive(Debug, Copy, Clone)] | ||
|
r45013 | pub enum BadType { | ||
CharacterDevice, | ||||
BlockDevice, | ||||
FIFO, | ||||
Socket, | ||||
Directory, | ||||
Unknown, | ||||
} | ||||
|
r47173 | impl fmt::Display for BadType { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
f.write_str(match self { | ||||
|
r45016 | BadType::CharacterDevice => "character device", | ||
BadType::BlockDevice => "block device", | ||||
BadType::FIFO => "fifo", | ||||
BadType::Socket => "socket", | ||||
BadType::Directory => "directory", | ||||
BadType::Unknown => "unknown", | ||||
|
r47173 | }) | ||
|
r45016 | } | ||
} | ||||
|
r45013 | /// Was explicitly matched but cannot be found/accessed | ||
|
r45023 | #[derive(Debug, Copy, Clone)] | ||
|
r45013 | pub enum BadMatch { | ||
OsError(i32), | ||||
BadType(BadType), | ||||
} | ||||
|
r45672 | /// Enum used to dispatch new status entries into the right collections. | ||
|
r44000 | /// Is similar to `crate::EntryState`, but represents the transient state of | ||
/// entries during the lifetime of a command. | ||||
|
r45023 | #[derive(Debug, Copy, Clone)] | ||
|
r45671 | pub enum Dispatch { | ||
|
r44000 | Unsure, | ||
Modified, | ||||
Added, | ||||
Removed, | ||||
Deleted, | ||||
Clean, | ||||
Unknown, | ||||
|
r45013 | Ignored, | ||
/// Empty dispatch, the file is not worth listing | ||||
None, | ||||
/// Was explicitly matched but cannot be found/accessed | ||||
Bad(BadMatch), | ||||
Directory { | ||||
/// True if the directory used to be a file in the dmap so we can say | ||||
/// that it's been removed. | ||||
was_file: bool, | ||||
}, | ||||
|
r44000 | } | ||
|
r44367 | type IoResult<T> = std::io::Result<T>; | ||
|
r45672 | |||
|
r47883 | /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add | ||
|
r45088 | /// an explicit lifetime here to not fight `'static` bounds "out of nowhere". | ||
|
r47882 | pub type IgnoreFnType<'a> = | ||
Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>; | ||||
|
r44367 | |||
|
r45672 | /// We have a good mix of owned (from directory traversal) and borrowed (from | ||
/// the dirstate/explicit) paths, this comes up a lot. | ||||
|
r45673 | pub type HgPathCow<'a> = Cow<'a, HgPath>; | ||
|
r45672 | |||
/// A path with its computed ``Dispatch`` information | ||||
type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch); | ||||
|
r46466 | /// The conversion from `HgPath` to a real fs path failed. | ||
/// `22` is the error code for "Invalid argument" | ||||
const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22)); | ||||
|
r44002 | /// Dates and times that are outside the 31-bit signed range are compared | ||
/// modulo 2^31. This should prevent hg from behaving badly with very large | ||||
/// files or corrupt dates while still having a high probability of detecting | ||||
/// changes. (issue2608) | ||||
/// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>` | ||||
/// is not defined for `i32`, and there is no `As` trait. This forces the | ||||
/// caller to cast `b` as `i32`. | ||||
fn mod_compare(a: i32, b: i32) -> bool { | ||||
a & i32::max_value() != b & i32::max_value() | ||||
} | ||||
|
r45010 | /// Return a sorted list containing information about the entries | ||
/// in the directory. | ||||
/// | ||||
/// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory | ||||
fn list_directory( | ||||
path: impl AsRef<Path>, | ||||
skip_dot_hg: bool, | ||||
) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> { | ||||
let mut results = vec![]; | ||||
let entries = read_dir(path.as_ref())?; | ||||
for entry in entries { | ||||
let entry = entry?; | ||||
let filename = os_string_to_hg_path_buf(entry.file_name())?; | ||||
let file_type = entry.file_type()?; | ||||
if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() { | ||||
return Ok(vec![]); | ||||
} else { | ||||
|
r45500 | results.push((filename, entry)) | ||
|
r45010 | } | ||
} | ||||
results.sort_unstable_by_key(|e| e.0.clone()); | ||||
Ok(results) | ||||
} | ||||
|
r44000 | /// The file corresponding to the dirstate entry was found on the filesystem. | ||
fn dispatch_found( | ||||
filename: impl AsRef<HgPath>, | ||||
entry: DirstateEntry, | ||||
metadata: HgMetadata, | ||||
copy_map: &CopyMap, | ||||
|
r45011 | options: StatusOptions, | ||
|
r44000 | ) -> Dispatch { | ||
let DirstateEntry { | ||||
state, | ||||
mode, | ||||
mtime, | ||||
size, | ||||
} = entry; | ||||
let HgMetadata { | ||||
st_mode, | ||||
st_size, | ||||
st_mtime, | ||||
.. | ||||
} = metadata; | ||||
match state { | ||||
EntryState::Normal => { | ||||
|
r44002 | let size_changed = mod_compare(size, st_size as i32); | ||
|
r44000 | let mode_changed = | ||
|
r45011 | (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec; | ||
|
r44002 | let metadata_changed = size >= 0 && (size_changed || mode_changed); | ||
|
r44003 | let other_parent = size == SIZE_FROM_OTHER_PARENT; | ||
|
r45500 | |||
|
r44002 | if metadata_changed | ||
|| other_parent | ||||
|| copy_map.contains_key(filename.as_ref()) | ||||
|
r44000 | { | ||
|
r47491 | if metadata.is_symlink() && size_changed { | ||
// issue6456: Size returned may be longer due to encryption | ||||
// on EXT-4 fscrypt. TODO maybe only do it on EXT4? | ||||
Dispatch::Unsure | ||||
} else { | ||||
Dispatch::Modified | ||||
} | ||||
|
r45500 | } else if mod_compare(mtime, st_mtime as i32) | ||
|| st_mtime == options.last_normal_time | ||||
{ | ||||
|
r44000 | // the file may have just been marked as normal and | ||
// it may have changed in the same second without | ||||
// changing its size. This can happen if we quickly | ||||
// do multiple commits. Force lookup, so we don't | ||||
// miss such a racy file change. | ||||
Dispatch::Unsure | ||||
|
r45011 | } else if options.list_clean { | ||
|
r44000 | Dispatch::Clean | ||
} else { | ||||
|
r45015 | Dispatch::None | ||
|
r44000 | } | ||
} | ||||
EntryState::Merged => Dispatch::Modified, | ||||
EntryState::Added => Dispatch::Added, | ||||
EntryState::Removed => Dispatch::Removed, | ||||
EntryState::Unknown => Dispatch::Unknown, | ||||
} | ||||
} | ||||
/// The file corresponding to this Dirstate entry is missing. | ||||
fn dispatch_missing(state: EntryState) -> Dispatch { | ||||
match state { | ||||
// File was removed from the filesystem during commands | ||||
EntryState::Normal | EntryState::Merged | EntryState::Added => { | ||||
Dispatch::Deleted | ||||
} | ||||
// File was removed, everything is normal | ||||
EntryState::Removed => Dispatch::Removed, | ||||
// File is unknown to Mercurial, everything is normal | ||||
EntryState::Unknown => Dispatch::Unknown, | ||||
} | ||||
} | ||||
|
r46466 | fn dispatch_os_error(e: &std::io::Error) -> Dispatch { | ||
Dispatch::Bad(BadMatch::OsError( | ||||
e.raw_os_error().expect("expected real OS error"), | ||||
)) | ||||
} | ||||
|
r45015 | lazy_static! { | ||
static ref DEFAULT_WORK: HashSet<&'static HgPath> = { | ||||
let mut h = HashSet::new(); | ||||
h.insert(HgPath::new(b"")); | ||||
h | ||||
}; | ||||
} | ||||
|
r45011 | #[derive(Debug, Copy, Clone)] | ||
pub struct StatusOptions { | ||||
/// Remember the most recent modification timeslot for status, to make | ||||
/// sure we won't miss future size-preserving file content modifications | ||||
/// that happen within the same timeslot. | ||||
pub last_normal_time: i64, | ||||
/// Whether we are on a filesystem with UNIX-like exec flags | ||||
pub check_exec: bool, | ||||
pub list_clean: bool, | ||||
|
r45014 | pub list_unknown: bool, | ||
pub list_ignored: bool, | ||||
|
r45353 | /// Whether to collect traversed dirs for applying a callback later. | ||
/// Used by `hg purge` for example. | ||||
pub collect_traversed_dirs: bool, | ||||
|
r45014 | } | ||
|
r47883 | #[derive(Debug, Default)] | ||
|
r45012 | pub struct DirstateStatus<'a> { | ||
|
r47881 | /// Tracked files whose contents have changed since the parent revision | ||
|
r45672 | pub modified: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// Newly-tracked files that were not present in the parent | ||||
|
r45672 | pub added: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// Previously-tracked files that have been (re)moved with an hg command | ||||
|
r45672 | pub removed: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// (Still) tracked files that are missing, (re)moved with an non-hg | ||||
/// command | ||||
|
r45672 | pub deleted: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// Tracked files that are up to date with the parent. | ||||
/// Only pupulated if `StatusOptions::list_clean` is true. | ||||
|
r45672 | pub clean: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// Files in the working directory that are ignored with `.hgignore`. | ||||
/// Only pupulated if `StatusOptions::list_ignored` is true. | ||||
|
r45672 | pub ignored: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// Files in the working directory that are neither tracked nor ignored. | ||||
/// Only pupulated if `StatusOptions::list_unknown` is true. | ||||
|
r45672 | pub unknown: Vec<HgPathCow<'a>>, | ||
|
r47881 | |||
/// Was explicitly matched but cannot be found/accessed | ||||
|
r45672 | pub bad: Vec<(HgPathCow<'a>, BadMatch)>, | ||
|
r47881 | |||
|
r47880 | /// Either clean or modified, but we can’t tell from filesystem metadata | ||
/// alone. The file contents need to be read and compared with that in | ||||
/// the parent. | ||||
pub unsure: Vec<HgPathCow<'a>>, | ||||
|
r47881 | |||
|
r45353 | /// Only filled if `collect_traversed_dirs` is `true` | ||
pub traversed: Vec<HgPathBuf>, | ||||
|
r43565 | } | ||
|
r47164 | #[derive(Debug, derive_more::From)] | ||
|
r45671 | pub enum StatusError { | ||
|
r45672 | /// Generic IO error | ||
|
r45671 | IO(std::io::Error), | ||
|
r45672 | /// An invalid path that cannot be represented in Mercurial was found | ||
|
r45671 | Path(HgPathError), | ||
|
r45672 | /// An invalid "ignore" pattern was found | ||
|
r45671 | Pattern(PatternError), | ||
|
r48126 | /// Corrupted dirstate | ||
DirstateV2ParseError(DirstateV2ParseError), | ||||
|
r45671 | } | ||
pub type StatusResult<T> = Result<T, StatusError>; | ||||
|
r47173 | impl fmt::Display for StatusError { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
|
r45671 | match self { | ||
|
r47173 | StatusError::IO(error) => error.fmt(f), | ||
StatusError::Path(error) => error.fmt(f), | ||||
StatusError::Pattern(error) => error.fmt(f), | ||||
|
r48126 | StatusError::DirstateV2ParseError(_) => { | ||
f.write_str("dirstate-v2 parse error") | ||||
} | ||||
|
r45671 | } | ||
} | ||||
} | ||||
|
r45672 | /// Gives information about which files are changed in the working directory | ||
/// and how, compared to the revision we're based on | ||||
|
r47863 | pub struct Status<'a, M: ?Sized + Matcher + Sync> { | ||
|
r45671 | dmap: &'a DirstateMap, | ||
|
r45673 | pub(crate) matcher: &'a M, | ||
|
r45671 | root_dir: PathBuf, | ||
|
r45673 | pub(crate) options: StatusOptions, | ||
|
r45671 | ignore_fn: IgnoreFnType<'a>, | ||
} | ||||
impl<'a, M> Status<'a, M> | ||||
where | ||||
|
r47863 | M: ?Sized + Matcher + Sync, | ||
|
r45671 | { | ||
pub fn new( | ||||
dmap: &'a DirstateMap, | ||||
matcher: &'a M, | ||||
root_dir: PathBuf, | ||||
ignore_files: Vec<PathBuf>, | ||||
options: StatusOptions, | ||||
) -> StatusResult<(Self, Vec<PatternFileWarning>)> { | ||||
// Needs to outlive `dir_ignore_fn` since it's captured. | ||||
let (ignore_fn, warnings): (IgnoreFnType, _) = | ||||
if options.list_ignored || options.list_unknown { | ||||
get_ignore_function(ignore_files, &root_dir)? | ||||
} else { | ||||
(Box::new(|&_| true), vec![]) | ||||
}; | ||||
Ok(( | ||||
Self { | ||||
dmap, | ||||
matcher, | ||||
root_dir, | ||||
options, | ||||
ignore_fn, | ||||
}, | ||||
warnings, | ||||
)) | ||||
} | ||||
|
r45672 | /// Is the path ignored? | ||
|
r45671 | pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool { | ||
(self.ignore_fn)(path.as_ref()) | ||||
} | ||||
/// Is the path or one of its ancestors ignored? | ||||
pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool { | ||||
// Only involve ignore mechanism if we're listing unknowns or ignored. | ||||
if self.options.list_ignored || self.options.list_unknown { | ||||
if self.is_ignored(&dir) { | ||||
true | ||||
} else { | ||||
for p in find_dirs(dir.as_ref()) { | ||||
if self.is_ignored(p) { | ||||
return true; | ||||
} | ||||
} | ||||
false | ||||
} | ||||
} else { | ||||
true | ||||
} | ||||
} | ||||
|
r45672 | /// Get stat data about the files explicitly specified by the matcher. | ||
/// Returns a tuple of the directories that need to be traversed and the | ||||
/// files with their corresponding `Dispatch`. | ||||
|
r45671 | /// TODO subrepos | ||
#[timed] | ||||
pub fn walk_explicit( | ||||
&self, | ||||
|
r46669 | traversed_sender: crossbeam_channel::Sender<HgPathBuf>, | ||
|
r45672 | ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) { | ||
|
r45671 | self.matcher | ||
.file_set() | ||||
.unwrap_or(&DEFAULT_WORK) | ||||
.par_iter() | ||||
|
r46466 | .flat_map(|&filename| -> Option<_> { | ||
|
r45671 | // TODO normalization | ||
let normalized = filename; | ||||
let buf = match hg_path_to_path_buf(normalized) { | ||||
Ok(x) => x, | ||||
|
r46466 | Err(_) => { | ||
return Some(( | ||||
Cow::Borrowed(normalized), | ||||
INVALID_PATH_DISPATCH, | ||||
)) | ||||
} | ||||
|
r45671 | }; | ||
let target = self.root_dir.join(buf); | ||||
let st = target.symlink_metadata(); | ||||
let in_dmap = self.dmap.get(normalized); | ||||
match st { | ||||
Ok(meta) => { | ||||
let file_type = meta.file_type(); | ||||
return if file_type.is_file() || file_type.is_symlink() | ||||
{ | ||||
if let Some(entry) = in_dmap { | ||||
|
r46466 | return Some(( | ||
|
r45671 | Cow::Borrowed(normalized), | ||
dispatch_found( | ||||
&normalized, | ||||
*entry, | ||||
HgMetadata::from_metadata(meta), | ||||
&self.dmap.copy_map, | ||||
self.options, | ||||
), | ||||
|
r46466 | )); | ||
|
r45671 | } | ||
|
r46466 | Some(( | ||
|
r45671 | Cow::Borrowed(normalized), | ||
Dispatch::Unknown, | ||||
|
r46466 | )) | ||
|
r45671 | } else if file_type.is_dir() { | ||
if self.options.collect_traversed_dirs { | ||||
traversed_sender | ||||
.send(normalized.to_owned()) | ||||
.expect("receiver should outlive sender"); | ||||
} | ||||
|
r46466 | Some(( | ||
|
r45671 | Cow::Borrowed(normalized), | ||
Dispatch::Directory { | ||||
was_file: in_dmap.is_some(), | ||||
}, | ||||
|
r46466 | )) | ||
|
r45671 | } else { | ||
|
r46466 | Some(( | ||
|
r45671 | Cow::Borrowed(normalized), | ||
Dispatch::Bad(BadMatch::BadType( | ||||
// TODO do more than unknown | ||||
// Support for all `BadType` variant | ||||
// varies greatly between platforms. | ||||
// So far, no tests check the type and | ||||
// this should be good enough for most | ||||
// users. | ||||
BadType::Unknown, | ||||
)), | ||||
|
r46466 | )) | ||
|
r45671 | }; | ||
} | ||||
Err(_) => { | ||||
if let Some(entry) = in_dmap { | ||||
|
r46466 | return Some(( | ||
|
r45671 | Cow::Borrowed(normalized), | ||
dispatch_missing(entry.state), | ||||
|
r46466 | )); | ||
|
r45671 | } | ||
} | ||||
}; | ||||
None | ||||
}) | ||||
.partition(|(_, dispatch)| match dispatch { | ||||
Dispatch::Directory { .. } => true, | ||||
_ => false, | ||||
}) | ||||
} | ||||
/// Walk the working directory recursively to look for changes compared to | ||||
/// the current `DirstateMap`. | ||||
/// | ||||
/// This takes a mutable reference to the results to account for the | ||||
/// `extend` in timings | ||||
#[timed] | ||||
pub fn traverse( | ||||
&self, | ||||
path: impl AsRef<HgPath>, | ||||
|
r45672 | old_results: &FastHashMap<HgPathCow<'a>, Dispatch>, | ||
results: &mut Vec<DispatchedPath<'a>>, | ||||
|
r46669 | traversed_sender: crossbeam_channel::Sender<HgPathBuf>, | ||
|
r46466 | ) { | ||
|
r45671 | // The traversal is done in parallel, so use a channel to gather | ||
|
r46669 | // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender` | ||
|
r45671 | // is not. | ||
let (files_transmitter, files_receiver) = | ||||
|
r46669 | crossbeam_channel::unbounded(); | ||
|
r45671 | |||
self.traverse_dir( | ||||
&files_transmitter, | ||||
path, | ||||
&old_results, | ||||
traversed_sender, | ||||
|
r46466 | ); | ||
|
r45671 | |||
// Disconnect the channel so the receiver stops waiting | ||||
drop(files_transmitter); | ||||
|
r46466 | let new_results = files_receiver | ||
.into_iter() | ||||
.par_bridge() | ||||
.map(|(f, d)| (Cow::Owned(f), d)); | ||||
|
r45671 | |||
|
r46466 | results.par_extend(new_results); | ||
|
r45671 | } | ||
/// Dispatch a single entry (file, folder, symlink...) found during | ||||
/// `traverse`. If the entry is a folder that needs to be traversed, it | ||||
/// will be handled in a separate thread. | ||||
fn handle_traversed_entry<'b>( | ||||
&'a self, | ||||
scope: &rayon::Scope<'b>, | ||||
|
r46669 | files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>, | ||
|
r45671 | old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>, | ||
filename: HgPathBuf, | ||||
dir_entry: DirEntry, | ||||
|
r46669 | traversed_sender: crossbeam_channel::Sender<HgPathBuf>, | ||
|
r45671 | ) -> IoResult<()> | ||
where | ||||
'a: 'b, | ||||
{ | ||||
let file_type = dir_entry.file_type()?; | ||||
let entry_option = self.dmap.get(&filename); | ||||
if filename.as_bytes() == b".hg" { | ||||
// Could be a directory or a symlink | ||||
return Ok(()); | ||||
} | ||||
if file_type.is_dir() { | ||||
self.handle_traversed_dir( | ||||
scope, | ||||
files_sender, | ||||
old_results, | ||||
entry_option, | ||||
filename, | ||||
traversed_sender, | ||||
); | ||||
} else if file_type.is_file() || file_type.is_symlink() { | ||||
if let Some(entry) = entry_option { | ||||
if self.matcher.matches_everything() | ||||
|| self.matcher.matches(&filename) | ||||
{ | ||||
let metadata = dir_entry.metadata()?; | ||||
files_sender | ||||
|
r46466 | .send(( | ||
|
r45671 | filename.to_owned(), | ||
dispatch_found( | ||||
&filename, | ||||
*entry, | ||||
HgMetadata::from_metadata(metadata), | ||||
&self.dmap.copy_map, | ||||
self.options, | ||||
), | ||||
|
r46466 | )) | ||
|
r45671 | .unwrap(); | ||
} | ||||
} else if (self.matcher.matches_everything() | ||||
|| self.matcher.matches(&filename)) | ||||
&& !self.is_ignored(&filename) | ||||
{ | ||||
if (self.options.list_ignored | ||||
|| self.matcher.exact_match(&filename)) | ||||
&& self.dir_ignore(&filename) | ||||
{ | ||||
if self.options.list_ignored { | ||||
files_sender | ||||
|
r46466 | .send((filename.to_owned(), Dispatch::Ignored)) | ||
|
r45671 | .unwrap(); | ||
} | ||||
} else if self.options.list_unknown { | ||||
files_sender | ||||
|
r46466 | .send((filename.to_owned(), Dispatch::Unknown)) | ||
|
r45671 | .unwrap(); | ||
} | ||||
} else if self.is_ignored(&filename) && self.options.list_ignored { | ||||
|
r48102 | if self.matcher.matches(&filename) { | ||
files_sender | ||||
.send((filename.to_owned(), Dispatch::Ignored)) | ||||
.unwrap(); | ||||
} | ||||
|
r45671 | } | ||
} else if let Some(entry) = entry_option { | ||||
// Used to be a file or a folder, now something else. | ||||
if self.matcher.matches_everything() | ||||
|| self.matcher.matches(&filename) | ||||
{ | ||||
files_sender | ||||
|
r46466 | .send((filename.to_owned(), dispatch_missing(entry.state))) | ||
|
r45671 | .unwrap(); | ||
} | ||||
} | ||||
Ok(()) | ||||
} | ||||
/// A directory was found in the filesystem and needs to be traversed | ||||
fn handle_traversed_dir<'b>( | ||||
&'a self, | ||||
scope: &rayon::Scope<'b>, | ||||
|
r46669 | files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>, | ||
|
r45671 | old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>, | ||
entry_option: Option<&'a DirstateEntry>, | ||||
directory: HgPathBuf, | ||||
|
r46669 | traversed_sender: crossbeam_channel::Sender<HgPathBuf>, | ||
|
r45671 | ) where | ||
'a: 'b, | ||||
{ | ||||
scope.spawn(move |_| { | ||||
// Nested `if` until `rust-lang/rust#53668` is stable | ||||
if let Some(entry) = entry_option { | ||||
// Used to be a file, is now a folder | ||||
if self.matcher.matches_everything() | ||||
|| self.matcher.matches(&directory) | ||||
{ | ||||
files_sender | ||||
|
r46466 | .send(( | ||
|
r45671 | directory.to_owned(), | ||
dispatch_missing(entry.state), | ||||
|
r46466 | )) | ||
|
r45671 | .unwrap(); | ||
} | ||||
} | ||||
// Do we need to traverse it? | ||||
if !self.is_ignored(&directory) || self.options.list_ignored { | ||||
self.traverse_dir( | ||||
files_sender, | ||||
directory, | ||||
&old_results, | ||||
traversed_sender, | ||||
) | ||||
} | ||||
}); | ||||
} | ||||
/// Decides whether the directory needs to be listed, and if so handles the | ||||
/// entries in a separate thread. | ||||
fn traverse_dir( | ||||
&self, | ||||
|
r46669 | files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>, | ||
|
r45671 | directory: impl AsRef<HgPath>, | ||
old_results: &FastHashMap<Cow<HgPath>, Dispatch>, | ||||
|
r46669 | traversed_sender: crossbeam_channel::Sender<HgPathBuf>, | ||
|
r46466 | ) { | ||
|
r45671 | let directory = directory.as_ref(); | ||
if self.options.collect_traversed_dirs { | ||||
traversed_sender | ||||
.send(directory.to_owned()) | ||||
.expect("receiver should outlive sender"); | ||||
} | ||||
let visit_entries = match self.matcher.visit_children_set(directory) { | ||||
|
r46466 | VisitChildrenSet::Empty => return, | ||
|
r45671 | VisitChildrenSet::This | VisitChildrenSet::Recursive => None, | ||
VisitChildrenSet::Set(set) => Some(set), | ||||
}; | ||||
|
r46466 | let buf = match hg_path_to_path_buf(directory) { | ||
Ok(b) => b, | ||||
Err(_) => { | ||||
files_sender | ||||
.send((directory.to_owned(), INVALID_PATH_DISPATCH)) | ||||
.expect("receiver should outlive sender"); | ||||
return; | ||||
} | ||||
}; | ||||
|
r45671 | let dir_path = self.root_dir.join(buf); | ||
let skip_dot_hg = !directory.as_bytes().is_empty(); | ||||
let entries = match list_directory(dir_path, skip_dot_hg) { | ||||
|
r45672 | Err(e) => { | ||
|
r46466 | files_sender | ||
.send((directory.to_owned(), dispatch_os_error(&e))) | ||||
.expect("receiver should outlive sender"); | ||||
return; | ||||
|
r45672 | } | ||
|
r45671 | Ok(entries) => entries, | ||
}; | ||||
|
r46466 | rayon::scope(|scope| { | ||
|
r45671 | for (filename, dir_entry) in entries { | ||
if let Some(ref set) = visit_entries { | ||||
if !set.contains(filename.deref()) { | ||||
continue; | ||||
} | ||||
} | ||||
// TODO normalize | ||||
let filename = if directory.is_empty() { | ||||
filename.to_owned() | ||||
} else { | ||||
directory.join(&filename) | ||||
}; | ||||
if !old_results.contains_key(filename.deref()) { | ||||
|
r46466 | match self.handle_traversed_entry( | ||
|
r45671 | scope, | ||
files_sender, | ||||
old_results, | ||||
filename, | ||||
dir_entry, | ||||
traversed_sender.clone(), | ||||
|
r46466 | ) { | ||
Err(e) => { | ||||
files_sender | ||||
.send(( | ||||
directory.to_owned(), | ||||
dispatch_os_error(&e), | ||||
)) | ||||
.expect("receiver should outlive sender"); | ||||
} | ||||
Ok(_) => {} | ||||
} | ||||
|
r45671 | } | ||
} | ||||
}) | ||||
} | ||||
|
r46185 | /// Add the files in the dirstate to the results. | ||
/// | ||||
/// This takes a mutable reference to the results to account for the | ||||
/// `extend` in timings | ||||
#[timed] | ||||
pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) { | ||||
|
r47218 | results.par_extend( | ||
self.dmap | ||||
.par_iter() | ||||
.filter(|(path, _)| self.matcher.matches(path)) | ||||
.map(move |(filename, entry)| { | ||||
let filename: &HgPath = filename; | ||||
let filename_as_path = match hg_path_to_path_buf(filename) | ||||
{ | ||||
Ok(f) => f, | ||||
Err(_) => { | ||||
return ( | ||||
Cow::Borrowed(filename), | ||||
INVALID_PATH_DISPATCH, | ||||
) | ||||
} | ||||
}; | ||||
let meta = self | ||||
.root_dir | ||||
.join(filename_as_path) | ||||
.symlink_metadata(); | ||||
match meta { | ||||
Ok(m) | ||||
if !(m.file_type().is_file() | ||||
|| m.file_type().is_symlink()) => | ||||
{ | ||||
( | ||||
Cow::Borrowed(filename), | ||||
dispatch_missing(entry.state), | ||||
) | ||||
} | ||||
Ok(m) => ( | ||||
|
r46466 | Cow::Borrowed(filename), | ||
|
r47218 | dispatch_found( | ||
filename, | ||||
*entry, | ||||
HgMetadata::from_metadata(m), | ||||
&self.dmap.copy_map, | ||||
self.options, | ||||
), | ||||
), | ||||
Err(e) | ||||
if e.kind() == ErrorKind::NotFound | ||||
|| e.raw_os_error() == Some(20) => | ||||
{ | ||||
// Rust does not yet have an `ErrorKind` for | ||||
// `NotADirectory` (errno 20) | ||||
// It happens if the dirstate contains `foo/bar` | ||||
// and foo is not a | ||||
// directory | ||||
( | ||||
Cow::Borrowed(filename), | ||||
dispatch_missing(entry.state), | ||||
) | ||||
} | ||||
Err(e) => { | ||||
(Cow::Borrowed(filename), dispatch_os_error(&e)) | ||||
} | ||||
|
r46185 | } | ||
|
r47218 | }), | ||
); | ||||
|
r46185 | } | ||
|
r45672 | /// Checks all files that are in the dirstate but were not found during the | ||
/// working directory traversal. This means that the rest must | ||||
/// be either ignored, under a symlink or under a new nested repo. | ||||
/// | ||||
|
r45671 | /// This takes a mutable reference to the results to account for the | ||
/// `extend` in timings | ||||
#[timed] | ||||
|
r46466 | pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) { | ||
|
r45671 | let to_visit: Vec<(&HgPath, &DirstateEntry)> = | ||
if results.is_empty() && self.matcher.matches_everything() { | ||||
self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect() | ||||
} else { | ||||
// Only convert to a hashmap if needed. | ||||
let old_results: FastHashMap<_, _> = | ||||
results.iter().cloned().collect(); | ||||
self.dmap | ||||
.iter() | ||||
.filter_map(move |(f, e)| { | ||||
if !old_results.contains_key(f.deref()) | ||||
&& self.matcher.matches(f) | ||||
{ | ||||
Some((f.deref(), e)) | ||||
} else { | ||||
None | ||||
} | ||||
}) | ||||
.collect() | ||||
}; | ||||
let path_auditor = PathAuditor::new(&self.root_dir); | ||||
|
r46466 | let new_results = to_visit.into_par_iter().filter_map( | ||
|(filename, entry)| -> Option<_> { | ||||
|
r45671 | // Report ignored items in the dmap as long as they are not | ||
// under a symlink directory. | ||||
if path_auditor.check(filename) { | ||||
// TODO normalize for case-insensitive filesystems | ||||
let buf = match hg_path_to_path_buf(filename) { | ||||
Ok(x) => x, | ||||
|
r46466 | Err(_) => { | ||
return Some(( | ||||
Cow::Owned(filename.to_owned()), | ||||
INVALID_PATH_DISPATCH, | ||||
)); | ||||
} | ||||
|
r45671 | }; | ||
|
r46466 | Some(( | ||
Cow::Owned(filename.to_owned()), | ||||
|
r45671 | match self.root_dir.join(&buf).symlink_metadata() { | ||
// File was just ignored, no links, and exists | ||||
Ok(meta) => { | ||||
let metadata = HgMetadata::from_metadata(meta); | ||||
dispatch_found( | ||||
filename, | ||||
*entry, | ||||
metadata, | ||||
&self.dmap.copy_map, | ||||
self.options, | ||||
) | ||||
} | ||||
// File doesn't exist | ||||
Err(_) => dispatch_missing(entry.state), | ||||
}, | ||||
|
r46466 | )) | ||
|
r45671 | } else { | ||
// It's either missing or under a symlink directory which | ||||
// we, in this case, report as missing. | ||||
|
r46466 | Some(( | ||
Cow::Owned(filename.to_owned()), | ||||
|
r45671 | dispatch_missing(entry.state), | ||
|
r46466 | )) | ||
|
r45671 | } | ||
|
r46466 | }, | ||
); | ||||
|
r45671 | |||
|
r46466 | results.par_extend(new_results); | ||
|
r45671 | } | ||
} | ||||
|
r45028 | #[timed] | ||
|
r45673 | pub fn build_response<'a>( | ||
|
r45672 | results: impl IntoIterator<Item = DispatchedPath<'a>>, | ||
|
r45353 | traversed: Vec<HgPathBuf>, | ||
|
r47880 | ) -> DirstateStatus<'a> { | ||
let mut unsure = vec![]; | ||||
|
r43565 | let mut modified = vec![]; | ||
let mut added = vec![]; | ||||
let mut removed = vec![]; | ||||
let mut deleted = vec![]; | ||||
let mut clean = vec![]; | ||||
|
r45013 | let mut ignored = vec![]; | ||
let mut unknown = vec![]; | ||||
let mut bad = vec![]; | ||||
|
r43565 | |||
|
r45015 | for (filename, dispatch) in results.into_iter() { | ||
|
r44000 | match dispatch { | ||
|
r45013 | Dispatch::Unknown => unknown.push(filename), | ||
|
r47880 | Dispatch::Unsure => unsure.push(filename), | ||
|
r44000 | Dispatch::Modified => modified.push(filename), | ||
Dispatch::Added => added.push(filename), | ||||
Dispatch::Removed => removed.push(filename), | ||||
Dispatch::Deleted => deleted.push(filename), | ||||
Dispatch::Clean => clean.push(filename), | ||||
|
r45013 | Dispatch::Ignored => ignored.push(filename), | ||
Dispatch::None => {} | ||||
Dispatch::Bad(reason) => bad.push((filename, reason)), | ||||
Dispatch::Directory { .. } => {} | ||||
|
r43565 | } | ||
} | ||||
|
r47880 | DirstateStatus { | ||
modified, | ||||
added, | ||||
removed, | ||||
deleted, | ||||
clean, | ||||
ignored, | ||||
unknown, | ||||
bad, | ||||
unsure, | ||||
traversed, | ||||
} | ||||
|
r45015 | } | ||
/// Get the status of files in the working directory. | ||||
/// | ||||
/// This is the current entry-point for `hg-core` and is realistically unusable | ||||
/// outside of a Python context because its arguments need to provide a lot of | ||||
/// information that will not be necessary in the future. | ||||
|
r45028 | #[timed] | ||
|
r45671 | pub fn status<'a>( | ||
|
r44367 | dmap: &'a DirstateMap, | ||
|
r47863 | matcher: &'a (dyn Matcher + Sync), | ||
|
r45671 | root_dir: PathBuf, | ||
|
r45088 | ignore_files: Vec<PathBuf>, | ||
|
r45011 | options: StatusOptions, | ||
|
r47880 | ) -> StatusResult<(DirstateStatus<'a>, Vec<PatternFileWarning>)> { | ||
|
r45673 | let (status, warnings) = | ||
|
r45671 | Status::new(dmap, matcher, root_dir, ignore_files, options)?; | ||
|
r45353 | |||
|
r45673 | Ok((status.run()?, warnings)) | ||
|
r43565 | } | ||