Commit 7528699c authored by Raphaël Gomès's avatar Raphaël Gomès
Browse files

rust-status: refactor status into a struct

The code for `dirstate/status` has grown too large for comfort, this is the
first of three patches that try to improve maintainability.
In this patch, refactoring dirstate's status into a struct allows for slimming
down function signatures drastically, keeping the mental (and maintenance)
burden lower, since pretty much all of them shared a few common arguments.

This had the pleasant side-effect of simplifying lifetimes a little. This has
no observable impact on performance.

The next patch will add/improve documentation and refactor some types. I tried
to keep new code down to a minimum in this patch because it's already pretty
big.

Differential Revision: https://phab.mercurial-scm.org/D8661
parent 503d0dd2af18
......@@ -73,7 +73,7 @@
/// Is similar to `crate::EntryState`, but represents the transient state of
/// entries during the lifetime of a command.
#[derive(Debug, Copy, Clone)]
enum Dispatch {
pub enum Dispatch {
Unsure,
Modified,
Added,
......@@ -214,88 +214,6 @@
};
}
/// Get stat data about the files explicitly specified by match.
/// TODO subrepos
#[timed]
fn walk_explicit<'a>(
files: Option<&'a HashSet<&HgPath>>,
dmap: &'a DirstateMap,
root_dir: impl AsRef<Path> + Sync + Send + 'a,
options: StatusOptions,
traversed_sender: crossbeam::Sender<HgPathBuf>,
) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
files
.unwrap_or(&DEFAULT_WORK)
.par_iter()
.map(move |&filename| {
// TODO normalization
let normalized = filename;
let buf = match hg_path_to_path_buf(normalized) {
Ok(x) => x,
Err(e) => return Some(Err(e.into())),
};
let target = root_dir.as_ref().join(buf);
let st = target.symlink_metadata();
let in_dmap = 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 {
return Some(Ok((
normalized,
dispatch_found(
&normalized,
*entry,
HgMetadata::from_metadata(meta),
&dmap.copy_map,
options,
),
)));
}
Some(Ok((normalized, Dispatch::Unknown)))
} else if file_type.is_dir() {
if options.collect_traversed_dirs {
traversed_sender
.send(normalized.to_owned())
.expect("receiver should outlive sender");
}
Some(Ok((
normalized,
Dispatch::Directory {
was_file: in_dmap.is_some(),
},
)))
} else {
Some(Ok((
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,
)),
)))
};
}
Err(_) => {
if let Some(entry) = in_dmap {
return Some(Ok((
normalized,
dispatch_missing(entry.state),
)));
}
}
};
None
})
.flatten()
}
#[derive(Debug, Copy, Clone)]
pub struct StatusOptions {
/// Remember the most recent modification timeslot for status, to make
......@@ -312,344 +230,6 @@
pub collect_traversed_dirs: bool,
}
/// 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<'a>(
scope: &rayon::Scope<'a>,
files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
matcher: &'a (impl Matcher + Sync),
root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
dmap: &'a DirstateMap,
old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
ignore_fn: &'a IgnoreFnType,
dir_ignore_fn: &'a IgnoreFnType,
options: StatusOptions,
filename: HgPathBuf,
dir_entry: DirEntry,
traversed_sender: crossbeam::Sender<HgPathBuf>,
) -> IoResult<()> {
let file_type = dir_entry.file_type()?;
let entry_option = dmap.get(&filename);
if filename.as_bytes() == b".hg" {
// Could be a directory or a symlink
return Ok(());
}
if file_type.is_dir() {
handle_traversed_dir(
scope,
files_sender,
matcher,
root_dir,
dmap,
old_results,
ignore_fn,
dir_ignore_fn,
options,
entry_option,
filename,
traversed_sender,
);
} else if file_type.is_file() || file_type.is_symlink() {
if let Some(entry) = entry_option {
if matcher.matches_everything() || matcher.matches(&filename) {
let metadata = dir_entry.metadata()?;
files_sender
.send(Ok((
filename.to_owned(),
dispatch_found(
&filename,
*entry,
HgMetadata::from_metadata(metadata),
&dmap.copy_map,
options,
),
)))
.unwrap();
}
} else if (matcher.matches_everything() || matcher.matches(&filename))
&& !ignore_fn(&filename)
{
if (options.list_ignored || matcher.exact_match(&filename))
&& dir_ignore_fn(&filename)
{
if options.list_ignored {
files_sender
.send(Ok((filename.to_owned(), Dispatch::Ignored)))
.unwrap();
}
} else if options.list_unknown {
files_sender
.send(Ok((filename.to_owned(), Dispatch::Unknown)))
.unwrap();
}
} else if ignore_fn(&filename) && options.list_ignored {
files_sender
.send(Ok((filename.to_owned(), Dispatch::Ignored)))
.unwrap();
}
} else if let Some(entry) = entry_option {
// Used to be a file or a folder, now something else.
if matcher.matches_everything() || matcher.matches(&filename) {
files_sender
.send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
.unwrap();
}
}
Ok(())
}
/// A directory was found in the filesystem and needs to be traversed
fn handle_traversed_dir<'a>(
scope: &rayon::Scope<'a>,
files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
matcher: &'a (impl Matcher + Sync),
root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
dmap: &'a DirstateMap,
old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
ignore_fn: &'a IgnoreFnType,
dir_ignore_fn: &'a IgnoreFnType,
options: StatusOptions,
entry_option: Option<&'a DirstateEntry>,
directory: HgPathBuf,
traversed_sender: crossbeam::Sender<HgPathBuf>,
) {
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 matcher.matches_everything() || matcher.matches(&directory) {
files_sender
.send(Ok((
directory.to_owned(),
dispatch_missing(entry.state),
)))
.unwrap();
}
}
// Do we need to traverse it?
if !ignore_fn(&directory) || options.list_ignored {
traverse_dir(
files_sender,
matcher,
root_dir,
dmap,
directory,
&old_results,
ignore_fn,
dir_ignore_fn,
options,
traversed_sender,
)
.unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
}
});
}
/// Decides whether the directory needs to be listed, and if so handles the
/// entries in a separate thread.
fn traverse_dir<'a>(
files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
matcher: &'a (impl Matcher + Sync),
root_dir: impl AsRef<Path> + Sync + Send + Copy,
dmap: &'a DirstateMap,
directory: impl AsRef<HgPath>,
old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
ignore_fn: &IgnoreFnType,
dir_ignore_fn: &IgnoreFnType,
options: StatusOptions,
traversed_sender: crossbeam::Sender<HgPathBuf>,
) -> IoResult<()> {
let directory = directory.as_ref();
if options.collect_traversed_dirs {
traversed_sender
.send(directory.to_owned())
.expect("receiver should outlive sender");
}
let visit_entries = match matcher.visit_children_set(directory) {
VisitChildrenSet::Empty => return Ok(()),
VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
VisitChildrenSet::Set(set) => Some(set),
};
let buf = hg_path_to_path_buf(directory)?;
let dir_path = root_dir.as_ref().join(buf);
let skip_dot_hg = !directory.as_bytes().is_empty();
let entries = match list_directory(dir_path, skip_dot_hg) {
Err(e) => match e.kind() {
ErrorKind::NotFound | ErrorKind::PermissionDenied => {
files_sender
.send(Ok((
directory.to_owned(),
Dispatch::Bad(BadMatch::OsError(
// Unwrapping here is OK because the error always
// is a real os error
e.raw_os_error().unwrap(),
)),
)))
.unwrap();
return Ok(());
}
_ => return Err(e),
},
Ok(entries) => entries,
};
rayon::scope(|scope| -> IoResult<()> {
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()) {
handle_traversed_entry(
scope,
files_sender,
matcher,
root_dir,
dmap,
old_results,
ignore_fn,
dir_ignore_fn,
options,
filename,
dir_entry,
traversed_sender.clone(),
)?;
}
}
Ok(())
})
}
/// 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]
fn traverse<'a>(
matcher: &'a (impl Matcher + Sync),
root_dir: impl AsRef<Path> + Sync + Send + Copy,
dmap: &'a DirstateMap,
path: impl AsRef<HgPath>,
old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
ignore_fn: &IgnoreFnType,
dir_ignore_fn: &IgnoreFnType,
options: StatusOptions,
results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
traversed_sender: crossbeam::Sender<HgPathBuf>,
) -> IoResult<()> {
let root_dir = root_dir.as_ref();
// The traversal is done in parallel, so use a channel to gather entries.
// `crossbeam::Sender` is `Sync`, while `mpsc::Sender` is not.
let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
traverse_dir(
&files_transmitter,
matcher,
root_dir,
&dmap,
path,
&old_results,
&ignore_fn,
&dir_ignore_fn,
options,
traversed_sender,
)?;
// Disconnect the channel so the receiver stops waiting
drop(files_transmitter);
// TODO don't collect. Find a way of replicating the behavior of
// `itertools::process_results`, but for `rayon::ParallelIterator`
let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
files_receiver
.into_iter()
.map(|item| {
let (f, d) = item?;
Ok((Cow::Owned(f), d))
})
.collect();
results.par_extend(new_results?);
Ok(())
}
/// Stat all entries in the `DirstateMap` and mark them for dispatch.
fn stat_dmap_entries(
dmap: &DirstateMap,
root_dir: impl AsRef<Path> + Sync + Send,
options: StatusOptions,
) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
dmap.par_iter().map(move |(filename, entry)| {
let filename: &HgPath = filename;
let filename_as_path = hg_path_to_path_buf(filename)?;
let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
match meta {
Ok(ref m)
if !(m.file_type().is_file()
|| m.file_type().is_symlink()) =>
{
Ok((filename, dispatch_missing(entry.state)))
}
Ok(m) => Ok((
filename,
dispatch_found(
filename,
*entry,
HgMetadata::from_metadata(m),
&dmap.copy_map,
options,
),
)),
Err(ref 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
Ok((filename, dispatch_missing(entry.state)))
}
Err(e) => Err(e),
}
})
}
/// This takes a mutable reference to the results to account for the `extend`
/// in timings
#[timed]
fn extend_from_dmap<'a>(
dmap: &'a DirstateMap,
root_dir: impl AsRef<Path> + Sync + Send,
options: StatusOptions,
results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
) {
results.par_extend(
stat_dmap_entries(dmap, root_dir, options)
.flatten()
.map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
);
}
#[derive(Debug)]
pub struct DirstateStatus<'a> {
pub modified: Vec<Cow<'a, HgPath>>,
......@@ -664,6 +244,583 @@
pub traversed: Vec<HgPathBuf>,
}
#[derive(Debug)]
pub enum StatusError {
IO(std::io::Error),
Path(HgPathError),
Pattern(PatternError),
}
pub type StatusResult<T> = Result<T, StatusError>;
impl From<PatternError> for StatusError {
fn from(e: PatternError) -> Self {
StatusError::Pattern(e)
}
}
impl From<HgPathError> for StatusError {
fn from(e: HgPathError) -> Self {
StatusError::Path(e)
}
}
impl From<std::io::Error> for StatusError {
fn from(e: std::io::Error) -> Self {
StatusError::IO(e)
}
}
impl ToString for StatusError {
fn to_string(&self) -> String {
match self {
StatusError::IO(e) => e.to_string(),
StatusError::Path(e) => e.to_string(),
StatusError::Pattern(e) => e.to_string(),
}
}
}
pub struct Status<'a, M: Matcher + Sync> {
dmap: &'a DirstateMap,
matcher: &'a M,
root_dir: PathBuf,
options: StatusOptions,
ignore_fn: IgnoreFnType<'a>,
}
impl<'a, M> Status<'a, M>
where
M: Matcher + Sync,
{
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,
))
}
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
}
}
/// Get stat data about the files explicitly specified by match.
/// TODO subrepos
#[timed]
pub fn walk_explicit(
&self,
traversed_sender: crossbeam::Sender<HgPathBuf>,
) -> (
Vec<(Cow<'a, HgPath>, Dispatch)>,
Vec<(Cow<'a, HgPath>, Dispatch)>,
) {
self.matcher
.file_set()
.unwrap_or(&DEFAULT_WORK)
.par_iter()
.map(|&filename| -> Option<IoResult<_>> {
// TODO normalization
let normalized = filename;
let buf = match hg_path_to_path_buf(normalized) {
Ok(x) => x,
Err(e) => return Some(Err(e.into())),
};
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 {
return Some(Ok((
Cow::Borrowed(normalized),
dispatch_found(
&normalized,
*entry,
HgMetadata::from_metadata(meta),
&self.dmap.copy_map,
self.options,
),
)));
}
Some(Ok((
Cow::Borrowed(normalized),
Dispatch::Unknown,
)))
} else if file_type.is_dir() {
if self.options.collect_traversed_dirs {
traversed_sender
.send(normalized.to_owned())
.expect("receiver should outlive sender");
}
Some(Ok((
Cow::Borrowed(normalized),
Dispatch::Directory {
was_file: in_dmap.is_some(),
},
)))
} else {
Some(Ok((