##// END OF EJS Templates
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums They are used instead of `&Node` and `&ChildNodes` respectively. The `ChildNodes` type alias also becomes a similar enum. For now they only have one variant each, to be extended later. Adding enums now forces various use sites go through new methods instead of manipulating the underlying data structure directly. Differential Revision: https://phab.mercurial-scm.org/D10747

File last commit:

r48124:69530e5d default
r48124:69530e5d default
Show More
status.rs
413 lines | 14.4 KiB | application/rls-services+xml | RustLexer
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 use crate::dirstate::status::IgnoreFnType;
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882 use crate::dirstate_tree::dirstate_map::DirstateMap;
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 use crate::dirstate_tree::dirstate_map::NodeRef;
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 use crate::matchers::get_ignore_function;
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882 use crate::matchers::Matcher;
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 use crate::utils::files::get_bytes_from_os_string;
use crate::utils::hg_path::HgPath;
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 use crate::BadMatch;
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882 use crate::DirstateStatus;
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 use crate::EntryState;
use crate::HgPathBuf;
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882 use crate::PatternFileWarning;
use crate::StatusError;
use crate::StatusOptions;
Simon Sapin
dirstate-tree: Add #[timed] attribute to `status` and `DirstateMap::read`...
r47888 use micro_timer::timed;
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 use rayon::prelude::*;
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 use std::borrow::Cow;
use std::io;
use std::path::Path;
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882 use std::path::PathBuf;
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 use std::sync::Mutex;
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 /// Returns the status of the working directory compared to its parent
/// changeset.
///
/// This algorithm is based on traversing the filesystem tree (`fs` in function
/// and variable names) and dirstate tree at the same time. The core of this
/// traversal is the recursive `traverse_fs_directory_and_dirstate` function
/// and its use of `itertools::merge_join_by`. When reaching a path that only
/// exists in one of the two trees, depending on information requested by
/// `options` we may need to traverse the remaining subtree.
Simon Sapin
dirstate-tree: Add #[timed] attribute to `status` and `DirstateMap::read`...
r47888 #[timed]
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 pub fn status<'tree>(
dmap: &'tree mut DirstateMap,
matcher: &(dyn Matcher + Sync),
root_dir: PathBuf,
ignore_files: Vec<PathBuf>,
options: StatusOptions,
) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> {
let (ignore_fn, warnings): (IgnoreFnType, _) =
if options.list_ignored || options.list_unknown {
get_ignore_function(ignore_files, &root_dir)?
} else {
(Box::new(|&_| true), vec![])
};
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 let common = StatusCommon {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 options,
matcher,
ignore_fn,
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 outcome: Mutex::new(DirstateStatus::default()),
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 };
let is_at_repo_root = true;
let hg_path = HgPath::new("");
let has_ignored_ancestor = false;
common.traverse_fs_directory_and_dirstate(
has_ignored_ancestor,
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 dmap.root.as_ref(),
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 hg_path,
&root_dir,
is_at_repo_root,
);
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 Ok((common.outcome.into_inner().unwrap(), warnings))
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
/// Bag of random things needed by various parts of the algorithm. Reduces the
/// number of parameters passed to functions.
struct StatusCommon<'tree, 'a> {
options: StatusOptions,
matcher: &'a (dyn Matcher + Sync),
ignore_fn: IgnoreFnType<'a>,
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 outcome: Mutex<DirstateStatus<'tree>>,
Simon Sapin
dirstate-tree: Give to `status()` mutable access to the `DirstateMap`...
r47882 }
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883
impl<'tree, 'a> StatusCommon<'tree, 'a> {
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 fn read_dir(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 hg_path: &HgPath,
fs_path: &Path,
is_at_repo_root: bool,
) -> Result<Vec<DirEntry>, ()> {
DirEntry::read_dir(fs_path, is_at_repo_root).map_err(|error| {
let errno = error.raw_os_error().expect("expected real OS error");
self.outcome
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 .lock()
.unwrap()
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 .bad
.push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
})
}
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 fn traverse_fs_directory_and_dirstate(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 has_ignored_ancestor: bool,
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 dirstate_nodes: ChildNodesRef<'tree, '_>,
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 directory_hg_path: &'tree HgPath,
directory_fs_path: &Path,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 is_at_repo_root: bool,
) {
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 let mut fs_entries = if let Ok(entries) = self.read_dir(
directory_hg_path,
directory_fs_path,
is_at_repo_root,
) {
entries
} else {
return;
};
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883
// `merge_join_by` requires both its input iterators to be sorted:
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 let dirstate_nodes = dirstate_nodes.sorted();
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
// https://github.com/rust-lang/rust/issues/34162
fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 itertools::merge_join_by(
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 dirstate_nodes,
&fs_entries,
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 |dirstate_node, fs_entry| {
dirstate_node.base_name().cmp(&fs_entry.base_name)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 },
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 )
.par_bridge()
.for_each(|pair| {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 use itertools::EitherOrBoth::*;
match pair {
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 Both(dirstate_node, fs_entry) => {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 self.traverse_fs_and_dirstate(
fs_entry,
dirstate_node,
has_ignored_ancestor,
);
}
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 Left(dirstate_node) => {
self.traverse_dirstate_only(dirstate_node)
}
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 Right(fs_entry) => self.traverse_fs_only(
has_ignored_ancestor,
directory_hg_path,
fs_entry,
),
}
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 })
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
fn traverse_fs_and_dirstate(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 fs_entry: &DirEntry,
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 dirstate_node: NodeRef<'tree, '_>,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 has_ignored_ancestor: bool,
) {
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 let hg_path = dirstate_node.full_path();
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 let file_type = fs_entry.metadata.file_type();
let file_or_symlink = file_type.is_file() || file_type.is_symlink();
if !file_or_symlink {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 // If we previously had a file here, it was removed (with
// `hg rm` or similar) or deleted before it could be
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 // replaced by a directory or something else.
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 self.mark_removed_or_deleted_if_file(
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 dirstate_node.full_path(),
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 dirstate_node.state(),
);
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 }
if file_type.is_dir() {
if self.options.collect_traversed_dirs {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().traversed.push(hg_path.into())
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 }
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
let is_at_repo_root = false;
self.traverse_fs_directory_and_dirstate(
is_ignored,
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 dirstate_node.children(),
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 hg_path,
&fs_entry.full_path,
is_at_repo_root,
);
} else {
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 if file_or_symlink && self.matcher.matches(hg_path) {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 let full_path = Cow::from(hg_path);
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 if let Some(state) = dirstate_node.state() {
match state {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 EntryState::Added => {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().added.push(full_path)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 EntryState::Removed => self
.outcome
.lock()
.unwrap()
.removed
.push(full_path),
EntryState::Merged => self
.outcome
.lock()
.unwrap()
.modified
.push(full_path),
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 EntryState::Normal => {
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 self.handle_normal_file(&dirstate_node, fs_entry);
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
// This variant is not used in DirstateMap
// nodes
EntryState::Unknown => unreachable!(),
}
} else {
// `node.entry.is_none()` indicates a "directory"
// node, but the filesystem has a file
self.mark_unknown_or_ignored(
has_ignored_ancestor,
full_path,
)
}
}
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 for child_node in dirstate_node.children().iter() {
self.traverse_dirstate_only(child_node)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
}
}
/// A file with `EntryState::Normal` in the dirstate was found in the
/// filesystem
fn handle_normal_file(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 dirstate_node: &NodeRef<'tree, '_>,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 fs_entry: &DirEntry,
) {
// Keep the low 31 bits
fn truncate_u64(value: u64) -> i32 {
(value & 0x7FFF_FFFF) as i32
}
fn truncate_i64(value: i64) -> i32 {
(value & 0x7FFF_FFFF) as i32
}
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 let entry = dirstate_node
.entry()
.expect("handle_normal_file called with entry-less node");
let full_path = Cow::from(dirstate_node.full_path());
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 let mode_changed = || {
self.options.check_exec && entry.mode_changed(&fs_entry.metadata)
};
let size_changed = entry.size != truncate_u64(fs_entry.metadata.len());
if entry.size >= 0
&& size_changed
&& fs_entry.metadata.file_type().is_symlink()
{
// issue6456: Size returned may be longer due to encryption
// on EXT-4 fscrypt. TODO maybe only do it on EXT4?
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().unsure.push(full_path)
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 } else if dirstate_node.copy_source().is_some()
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 || entry.is_from_other_parent()
|| (entry.size >= 0 && (size_changed || mode_changed()))
{
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().modified.push(full_path)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 } else {
let mtime = mtime_seconds(&fs_entry.metadata);
if truncate_i64(mtime) != entry.mtime
|| mtime == self.options.last_normal_time
{
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().unsure.push(full_path)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 } else if self.options.list_clean {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().clean.push(full_path)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
}
}
/// A node in the dirstate tree has no corresponding filesystem entry
Simon Sapin
dirstate-tree: Add `NodeRef` and `ChildNodesRef` enums...
r48124 fn traverse_dirstate_only(&self, dirstate_node: NodeRef<'tree, '_>) {
self.mark_removed_or_deleted_if_file(
dirstate_node.full_path(),
dirstate_node.state(),
);
dirstate_node
.children()
.par_iter()
.for_each(|child_node| self.traverse_dirstate_only(child_node))
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
/// A node in the dirstate tree has no corresponding *file* on the
/// filesystem
///
/// Does nothing on a "directory" node
fn mark_removed_or_deleted_if_file(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 hg_path: &'tree HgPath,
dirstate_node_state: Option<EntryState>,
) {
if let Some(state) = dirstate_node_state {
if self.matcher.matches(hg_path) {
if let EntryState::Removed = state {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().removed.push(hg_path.into())
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 } else {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().deleted.push(hg_path.into())
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
}
}
}
/// Something in the filesystem has no corresponding dirstate node
fn traverse_fs_only(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 has_ignored_ancestor: bool,
directory_hg_path: &HgPath,
fs_entry: &DirEntry,
) {
let hg_path = directory_hg_path.join(&fs_entry.base_name);
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 let file_type = fs_entry.metadata.file_type();
let file_or_symlink = file_type.is_file() || file_type.is_symlink();
if file_type.is_dir() {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 let is_ignored =
has_ignored_ancestor || (self.ignore_fn)(&hg_path);
let traverse_children = if is_ignored {
// Descendants of an ignored directory are all ignored
self.options.list_ignored
} else {
// Descendants of an unknown directory may be either unknown or
// ignored
self.options.list_unknown || self.options.list_ignored
};
if traverse_children {
let is_at_repo_root = false;
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 if let Ok(children_fs_entries) = self.read_dir(
&hg_path,
&fs_entry.full_path,
is_at_repo_root,
) {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 children_fs_entries.par_iter().for_each(|child_fs_entry| {
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 self.traverse_fs_only(
is_ignored,
&hg_path,
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 child_fs_entry,
Simon Sapin
dirstate-tree: Handle I/O errors in status...
r47885 )
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 })
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
}
if self.options.collect_traversed_dirs {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().traversed.push(hg_path.into())
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
Simon Sapin
dirstate-tree: Ignore FIFOs etc. in the status algorithm...
r47884 } else if file_or_symlink && self.matcher.matches(&hg_path) {
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into())
}
}
fn mark_unknown_or_ignored(
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 &self,
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 has_ignored_ancestor: bool,
hg_path: Cow<'tree, HgPath>,
) {
let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
if is_ignored {
if self.options.list_ignored {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().ignored.push(hg_path)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
} else {
if self.options.list_unknown {
Simon Sapin
dirstate-tree: Paralellize the status algorithm with Rayon...
r47887 self.outcome.lock().unwrap().unknown.push(hg_path)
Simon Sapin
dirstate-tree: Add the new `status()` algorithm...
r47883 }
}
}
}
#[cfg(unix)] // TODO
fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
// Going through `Metadata::modified()` would be portable, but would take
// care to construct a `SystemTime` value with sub-second precision just
// for us to throw that away here.
use std::os::unix::fs::MetadataExt;
metadata.mtime()
}
struct DirEntry {
base_name: HgPathBuf,
full_path: PathBuf,
metadata: std::fs::Metadata,
}
impl DirEntry {
/// Returns **unsorted** entries in the given directory, with name and
/// metadata.
///
/// If a `.hg` sub-directory is encountered:
///
/// * At the repository root, ignore that sub-directory
/// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
/// list instead.
fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
let mut results = Vec::new();
for entry in path.read_dir()? {
let entry = entry?;
let metadata = entry.metadata()?;
let name = get_bytes_from_os_string(entry.file_name());
// FIXME don't do this when cached
if name == b".hg" {
if is_at_repo_root {
// Skip the repo’s own .hg (might be a symlink)
continue;
} else if metadata.is_dir() {
// A .hg sub-directory at another location means a subrepo,
// skip it entirely.
return Ok(Vec::new());
}
}
results.push(DirEntry {
base_name: name.into(),
full_path: entry.path(),
metadata,
})
}
Ok(results)
}
}