status.rs
770 lines
| 25.9 KiB
| application/rls-services+xml
|
RustLexer
Raphaël Gomès
|
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. | ||||
Raphaël Gomès
|
r44003 | use crate::{ | ||
dirstate::SIZE_FROM_OTHER_PARENT, | ||||
Raphaël Gomès
|
r45015 | filepatterns::PatternFileWarning, | ||
matchers::{get_ignore_function, Matcher, VisitChildrenSet}, | ||||
Raphaël Gomès
|
r44003 | utils::{ | ||
Raphaël Gomès
|
r45015 | files::{find_dirs, HgMetadata}, | ||
Raphaël Gomès
|
r45010 | hg_path::{ | ||
hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf, | ||||
Raphaël Gomès
|
r45015 | HgPathError, | ||
Raphaël Gomès
|
r45010 | }, | ||
Raphaël Gomès
|
r45015 | path_auditor::PathAuditor, | ||
Raphaël Gomès
|
r44003 | }, | ||
Raphaël Gomès
|
r45014 | CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap, | ||
Raphaël Gomès
|
r45015 | PatternError, | ||
Raphaël Gomès
|
r44003 | }; | ||
Raphaël Gomès
|
r45015 | use lazy_static::lazy_static; | ||
Raphaël Gomès
|
r43565 | use rayon::prelude::*; | ||
Raphaël Gomès
|
r45015 | use std::collections::VecDeque; | ||
use std::{ | ||||
borrow::Cow, | ||||
collections::HashSet, | ||||
fs::{read_dir, DirEntry}, | ||||
io::ErrorKind, | ||||
ops::Deref, | ||||
path::Path, | ||||
}; | ||||
Raphaël Gomès
|
r43565 | |||
Raphaël Gomès
|
r45013 | /// Wrong type of file from a `BadMatch` | ||
/// Note: a lot of those don't exist on all platforms. | ||||
#[derive(Debug)] | ||||
pub enum BadType { | ||||
CharacterDevice, | ||||
BlockDevice, | ||||
FIFO, | ||||
Socket, | ||||
Directory, | ||||
Unknown, | ||||
} | ||||
Raphaël Gomès
|
r45016 | impl ToString for BadType { | ||
fn to_string(&self) -> String { | ||||
match self { | ||||
BadType::CharacterDevice => "character device", | ||||
BadType::BlockDevice => "block device", | ||||
BadType::FIFO => "fifo", | ||||
BadType::Socket => "socket", | ||||
BadType::Directory => "directory", | ||||
BadType::Unknown => "unknown", | ||||
} | ||||
.to_string() | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r45013 | /// Was explicitly matched but cannot be found/accessed | ||
#[derive(Debug)] | ||||
pub enum BadMatch { | ||||
OsError(i32), | ||||
BadType(BadType), | ||||
} | ||||
Raphaël Gomès
|
r44000 | /// Marker enum used to dispatch new status entries into the right collections. | ||
/// Is similar to `crate::EntryState`, but represents the transient state of | ||||
/// entries during the lifetime of a command. | ||||
Raphaël Gomès
|
r45015 | #[derive(Debug)] | ||
Raphaël Gomès
|
r44000 | enum Dispatch { | ||
Unsure, | ||||
Modified, | ||||
Added, | ||||
Removed, | ||||
Deleted, | ||||
Clean, | ||||
Unknown, | ||||
Raphaël Gomès
|
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, | ||||
}, | ||||
Raphaël Gomès
|
r44000 | } | ||
Raphaël Gomès
|
r44367 | type IoResult<T> = std::io::Result<T>; | ||
Raphaël Gomès
|
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() | ||||
} | ||||
Raphaël Gomès
|
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 { | ||||
results.push((HgPathBuf::from(filename), entry)) | ||||
} | ||||
} | ||||
results.sort_unstable_by_key(|e| e.0.clone()); | ||||
Ok(results) | ||||
} | ||||
Raphaël Gomès
|
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, | ||||
Raphaël Gomès
|
r45011 | options: StatusOptions, | ||
Raphaël Gomès
|
r44000 | ) -> Dispatch { | ||
let DirstateEntry { | ||||
state, | ||||
mode, | ||||
mtime, | ||||
size, | ||||
} = entry; | ||||
let HgMetadata { | ||||
st_mode, | ||||
st_size, | ||||
st_mtime, | ||||
.. | ||||
} = metadata; | ||||
match state { | ||||
EntryState::Normal => { | ||||
Raphaël Gomès
|
r44002 | let size_changed = mod_compare(size, st_size as i32); | ||
Raphaël Gomès
|
r44000 | let mode_changed = | ||
Raphaël Gomès
|
r45011 | (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec; | ||
Raphaël Gomès
|
r44002 | let metadata_changed = size >= 0 && (size_changed || mode_changed); | ||
Raphaël Gomès
|
r44003 | let other_parent = size == SIZE_FROM_OTHER_PARENT; | ||
Raphaël Gomès
|
r44002 | if metadata_changed | ||
|| other_parent | ||||
|| copy_map.contains_key(filename.as_ref()) | ||||
Raphaël Gomès
|
r44000 | { | ||
Dispatch::Modified | ||||
Raphaël Gomès
|
r44002 | } else if mod_compare(mtime, st_mtime as i32) { | ||
Raphaël Gomès
|
r44000 | Dispatch::Unsure | ||
Raphaël Gomès
|
r45011 | } else if st_mtime == options.last_normal_time { | ||
Raphaël Gomès
|
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 | ||||
Raphaël Gomès
|
r45011 | } else if options.list_clean { | ||
Raphaël Gomès
|
r44000 | Dispatch::Clean | ||
} else { | ||||
Raphaël Gomès
|
r45015 | Dispatch::None | ||
Raphaël Gomès
|
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, | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r45015 | lazy_static! { | ||
static ref DEFAULT_WORK: HashSet<&'static HgPath> = { | ||||
let mut h = HashSet::new(); | ||||
h.insert(HgPath::new(b"")); | ||||
h | ||||
}; | ||||
} | ||||
Raphaël Gomès
|
r44367 | /// Get stat data about the files explicitly specified by match. | ||
/// TODO subrepos | ||||
fn walk_explicit<'a>( | ||||
Raphaël Gomès
|
r45015 | files: Option<&'a HashSet<&HgPath>>, | ||
Raphaël Gomès
|
r44367 | dmap: &'a DirstateMap, | ||
Raphaël Gomès
|
r45015 | root_dir: impl AsRef<Path> + Sync + Send + 'a, | ||
Raphaël Gomès
|
r45011 | options: StatusOptions, | ||
Raphaël Gomès
|
r44367 | ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> { | ||
Raphaël Gomès
|
r45015 | files | ||
.unwrap_or(&DEFAULT_WORK) | ||||
.par_iter() | ||||
.map(move |filename| { | ||||
// TODO normalization | ||||
let normalized = filename.as_ref(); | ||||
Raphaël Gomès
|
r44367 | |||
Raphaël Gomès
|
r45015 | 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() { | ||||
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 { | ||||
Raphaël Gomès
|
r44367 | return Some(Ok(( | ||
normalized, | ||||
Raphaël Gomès
|
r45015 | dispatch_missing(entry.state), | ||
Raphaël Gomès
|
r44367 | ))); | ||
} | ||||
} | ||||
Raphaël Gomès
|
r45015 | }; | ||
None | ||||
}) | ||||
.flatten() | ||||
Raphaël Gomès
|
r44367 | } | ||
Raphaël Gomès
|
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, | ||||
Raphaël Gomès
|
r45014 | pub list_unknown: bool, | ||
pub list_ignored: bool, | ||||
} | ||||
/// Dispatch a single file found during `traverse`. | ||||
/// If `file` is a folder that needs to be traversed, it will be pushed into | ||||
/// `work`. | ||||
fn traverse_worker<'a>( | ||||
work: &mut VecDeque<HgPathBuf>, | ||||
matcher: &impl Matcher, | ||||
dmap: &DirstateMap, | ||||
filename: impl AsRef<HgPath>, | ||||
dir_entry: &DirEntry, | ||||
ignore_fn: &impl for<'r> Fn(&'r HgPath) -> bool, | ||||
dir_ignore_fn: &impl for<'r> Fn(&'r HgPath) -> bool, | ||||
options: StatusOptions, | ||||
) -> Option<IoResult<(Cow<'a, HgPath>, Dispatch)>> { | ||||
let file_type = match dir_entry.file_type() { | ||||
Ok(x) => x, | ||||
Err(e) => return Some(Err(e.into())), | ||||
}; | ||||
let filename = filename.as_ref(); | ||||
let entry_option = dmap.get(filename); | ||||
if file_type.is_dir() { | ||||
// Do we need to traverse it? | ||||
if !ignore_fn(&filename) || options.list_ignored { | ||||
work.push_front(filename.to_owned()); | ||||
} | ||||
// 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(&filename) { | ||||
return Some(Ok(( | ||||
Cow::Owned(filename.to_owned()), | ||||
dispatch_missing(entry.state), | ||||
))); | ||||
} | ||||
} | ||||
} 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 = match dir_entry.metadata() { | ||||
Ok(x) => x, | ||||
Err(e) => return Some(Err(e.into())), | ||||
}; | ||||
return Some(Ok(( | ||||
Cow::Owned(filename.to_owned()), | ||||
dispatch_found( | ||||
&filename, | ||||
*entry, | ||||
HgMetadata::from_metadata(metadata), | ||||
&dmap.copy_map, | ||||
options, | ||||
), | ||||
))); | ||||
} | ||||
} 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 { | ||||
return Some(Ok(( | ||||
Cow::Owned(filename.to_owned()), | ||||
Dispatch::Ignored, | ||||
))); | ||||
} | ||||
} else { | ||||
return Some(Ok(( | ||||
Cow::Owned(filename.to_owned()), | ||||
Dispatch::Unknown, | ||||
))); | ||||
} | ||||
} | ||||
} 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) { | ||||
return Some(Ok(( | ||||
Cow::Owned(filename.to_owned()), | ||||
dispatch_missing(entry.state), | ||||
))); | ||||
} | ||||
} | ||||
None | ||||
} | ||||
/// Walk the working directory recursively to look for changes compared to the | ||||
/// current `DirstateMap`. | ||||
fn traverse<'a>( | ||||
matcher: &(impl Matcher + Sync), | ||||
root_dir: impl AsRef<Path>, | ||||
dmap: &DirstateMap, | ||||
path: impl AsRef<HgPath>, | ||||
old_results: FastHashMap<Cow<'a, HgPath>, Dispatch>, | ||||
ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync), | ||||
dir_ignore_fn: &(impl for<'r> Fn(&'r HgPath) -> bool + Sync), | ||||
options: StatusOptions, | ||||
) -> IoResult<FastHashMap<Cow<'a, HgPath>, Dispatch>> { | ||||
let root_dir = root_dir.as_ref(); | ||||
let mut new_results = FastHashMap::default(); | ||||
let mut work = VecDeque::new(); | ||||
work.push_front(path.as_ref().to_owned()); | ||||
while let Some(ref directory) = work.pop_front() { | ||||
if directory.as_bytes() == b".hg" { | ||||
continue; | ||||
} | ||||
let visit_entries = match matcher.visit_children_set(directory) { | ||||
VisitChildrenSet::Empty => continue, | ||||
VisitChildrenSet::This | VisitChildrenSet::Recursive => None, | ||||
VisitChildrenSet::Set(set) => Some(set), | ||||
}; | ||||
let buf = hg_path_to_path_buf(directory)?; | ||||
let dir_path = root_dir.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 => { | ||||
new_results.insert( | ||||
Cow::Owned(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(), | ||||
)), | ||||
); | ||||
continue; | ||||
} | ||||
_ => return Err(e), | ||||
}, | ||||
Ok(entries) => entries, | ||||
}; | ||||
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()) { | ||||
if let Some((res, dispatch)) = traverse_worker( | ||||
&mut work, | ||||
matcher, | ||||
&dmap, | ||||
&filename, | ||||
&dir_entry, | ||||
&ignore_fn, | ||||
&dir_ignore_fn, | ||||
options, | ||||
) | ||||
.transpose()? | ||||
{ | ||||
new_results.insert(res, dispatch); | ||||
} | ||||
} | ||||
} | ||||
} | ||||
new_results.extend(old_results.into_iter()); | ||||
Ok(new_results) | ||||
Raphaël Gomès
|
r45011 | } | ||
Raphaël Gomès
|
r45015 | /// Stat all entries in the `DirstateMap` and mark them for dispatch. | ||
Raphaël Gomès
|
r44000 | fn stat_dmap_entries( | ||
Raphaël Gomès
|
r43565 | dmap: &DirstateMap, | ||
Raphaël Gomès
|
r44001 | root_dir: impl AsRef<Path> + Sync + Send, | ||
Raphaël Gomès
|
r45011 | options: StatusOptions, | ||
Raphaël Gomès
|
r44367 | ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> { | ||
Raphaël Gomès
|
r44001 | 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(); | ||||
Raphaël Gomès
|
r43565 | |||
Raphaël Gomès
|
r44001 | 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, | ||||
Raphaël Gomès
|
r45011 | options, | ||
Raphaël Gomès
|
r44001 | ), | ||
)), | ||||
Err(ref e) | ||||
Raphaël Gomès
|
r45015 | if e.kind() == ErrorKind::NotFound | ||
Raphaël Gomès
|
r44001 | || 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), | ||||
} | ||||
}) | ||||
Raphaël Gomès
|
r43565 | } | ||
Raphaël Gomès
|
r45012 | pub struct DirstateStatus<'a> { | ||
Raphaël Gomès
|
r45015 | pub modified: Vec<Cow<'a, HgPath>>, | ||
pub added: Vec<Cow<'a, HgPath>>, | ||||
pub removed: Vec<Cow<'a, HgPath>>, | ||||
pub deleted: Vec<Cow<'a, HgPath>>, | ||||
pub clean: Vec<Cow<'a, HgPath>>, | ||||
pub ignored: Vec<Cow<'a, HgPath>>, | ||||
pub unknown: Vec<Cow<'a, HgPath>>, | ||||
pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>, | ||||
Raphaël Gomès
|
r43565 | } | ||
Raphaël Gomès
|
r44367 | fn build_response<'a>( | ||
Raphaël Gomès
|
r45015 | results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>, | ||
) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) { | ||||
Raphaël Gomès
|
r43565 | let mut lookup = vec![]; | ||
let mut modified = vec![]; | ||||
let mut added = vec![]; | ||||
let mut removed = vec![]; | ||||
let mut deleted = vec![]; | ||||
let mut clean = vec![]; | ||||
Raphaël Gomès
|
r45013 | let mut ignored = vec![]; | ||
let mut unknown = vec![]; | ||||
let mut bad = vec![]; | ||||
Raphaël Gomès
|
r43565 | |||
Raphaël Gomès
|
r45015 | for (filename, dispatch) in results.into_iter() { | ||
Raphaël Gomès
|
r44000 | match dispatch { | ||
Raphaël Gomès
|
r45013 | Dispatch::Unknown => unknown.push(filename), | ||
Raphaël Gomès
|
r44000 | Dispatch::Unsure => lookup.push(filename), | ||
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), | ||||
Raphaël Gomès
|
r45013 | Dispatch::Ignored => ignored.push(filename), | ||
Dispatch::None => {} | ||||
Dispatch::Bad(reason) => bad.push((filename, reason)), | ||||
Dispatch::Directory { .. } => {} | ||||
Raphaël Gomès
|
r43565 | } | ||
} | ||||
Raphaël Gomès
|
r45015 | ( | ||
Raphaël Gomès
|
r43565 | lookup, | ||
Raphaël Gomès
|
r45012 | DirstateStatus { | ||
Raphaël Gomès
|
r43565 | modified, | ||
added, | ||||
removed, | ||||
deleted, | ||||
clean, | ||||
Raphaël Gomès
|
r45013 | ignored, | ||
unknown, | ||||
bad, | ||||
Raphaël Gomès
|
r43565 | }, | ||
Raphaël Gomès
|
r45015 | ) | ||
} | ||||
pub enum StatusError { | ||||
IO(std::io::Error), | ||||
Path(HgPathError), | ||||
Pattern(PatternError), | ||||
Raphaël Gomès
|
r43565 | } | ||
Raphaël Gomès
|
r45015 | 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(), | ||||
} | ||||
} | ||||
} | ||||
/// 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. | ||||
Raphaël Gomès
|
r44367 | pub fn status<'a: 'c, 'b: 'c, 'c>( | ||
dmap: &'a DirstateMap, | ||||
Raphaël Gomès
|
r45015 | matcher: &'b (impl Matcher + Sync), | ||
root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c, | ||||
ignore_files: &[impl AsRef<Path> + 'c], | ||||
Raphaël Gomès
|
r45011 | options: StatusOptions, | ||
Raphaël Gomès
|
r45015 | ) -> StatusResult<( | ||
(Vec<Cow<'c, HgPath>>, DirstateStatus<'c>), | ||||
Vec<PatternFileWarning>, | ||||
)> { | ||||
let (ignore_fn, warnings) = get_ignore_function(&ignore_files, root_dir)?; | ||||
// Is the path or one of its ancestors ignored? | ||||
let dir_ignore_fn = |dir: &_| { | ||||
if ignore_fn(dir) { | ||||
true | ||||
} else { | ||||
for p in find_dirs(dir) { | ||||
if ignore_fn(p) { | ||||
return true; | ||||
} | ||||
} | ||||
false | ||||
} | ||||
}; | ||||
Raphaël Gomès
|
r44367 | let files = matcher.file_set(); | ||
Raphaël Gomès
|
r45015 | |||
// Step 1: check the files explicitly mentioned by the user | ||||
let explicit = walk_explicit(files, &dmap, root_dir, options); | ||||
let (work, mut results): (Vec<_>, FastHashMap<_, _>) = explicit | ||||
.filter_map(Result::ok) | ||||
.map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)) | ||||
.partition(|(_, dispatch)| match dispatch { | ||||
Dispatch::Directory { .. } => true, | ||||
_ => false, | ||||
}); | ||||
// Step 2: recursively check the working directory for changes if needed | ||||
for (dir, dispatch) in work { | ||||
match dispatch { | ||||
Dispatch::Directory { was_file } => { | ||||
if was_file { | ||||
results.insert(dir.to_owned(), Dispatch::Removed); | ||||
} | ||||
if options.list_ignored | ||||
|| options.list_unknown && !dir_ignore_fn(&dir) | ||||
{ | ||||
results = traverse( | ||||
matcher, | ||||
root_dir, | ||||
&dmap, | ||||
&dir, | ||||
results, | ||||
&ignore_fn, | ||||
&dir_ignore_fn, | ||||
options, | ||||
)?; | ||||
} | ||||
} | ||||
_ => unreachable!("There can only be directories in `work`"), | ||||
} | ||||
Raphaël Gomès
|
r44367 | } | ||
Raphaël Gomès
|
r44000 | |||
Raphaël Gomès
|
r44367 | if !matcher.is_exact() { | ||
Raphaël Gomès
|
r45015 | // Step 3: Check the remaining files from the dmap. | ||
// If a dmap file is not in results yet, it was either | ||||
// a) not matched b) ignored, c) missing, or d) under a | ||||
// symlink directory. | ||||
if options.list_unknown { | ||||
let to_visit: Box<dyn Iterator<Item = (&HgPath, &DirstateEntry)>> = | ||||
if results.is_empty() && matcher.matches_everything() { | ||||
Box::new(dmap.iter().map(|(f, e)| (f.deref(), e))) | ||||
} else { | ||||
Box::new(dmap.iter().filter_map(|(f, e)| { | ||||
if !results.contains_key(f.deref()) | ||||
&& matcher.matches(f) | ||||
{ | ||||
Some((f.deref(), e)) | ||||
} else { | ||||
None | ||||
} | ||||
})) | ||||
}; | ||||
let mut to_visit: Vec<_> = to_visit.collect(); | ||||
to_visit.sort_by(|a, b| a.0.cmp(&b.0)); | ||||
// We walked all dirs under the roots that weren't ignored, and | ||||
// everything that matched was stat'ed and is already in results. | ||||
// The rest must thus be ignored or under a symlink. | ||||
let mut path_auditor = PathAuditor::new(root_dir); | ||||
for (ref filename, entry) in to_visit { | ||||
// 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 = hg_path_to_path_buf(filename)?; | ||||
results.insert( | ||||
Cow::Borrowed(filename), | ||||
match root_dir.as_ref().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, | ||||
&dmap.copy_map, | ||||
options, | ||||
) | ||||
} | ||||
// File doesn't exist | ||||
Err(_) => dispatch_missing(entry.state), | ||||
}, | ||||
); | ||||
} else { | ||||
// It's either missing or under a symlink directory which | ||||
// we, in this case, report as missing. | ||||
results.insert( | ||||
Cow::Borrowed(filename), | ||||
dispatch_missing(entry.state), | ||||
); | ||||
} | ||||
} | ||||
} else { | ||||
// We may not have walked the full directory tree above, so stat | ||||
// and check everything we missed. | ||||
let stat_results = stat_dmap_entries(&dmap, root_dir, options); | ||||
results.par_extend(stat_results.flatten().map( | ||||
|(filename, dispatch)| (Cow::Borrowed(filename), dispatch), | ||||
)); | ||||
} | ||||
Raphaël Gomès
|
r44367 | } | ||
Raphaël Gomès
|
r45015 | let results = results.into_iter().filter_map(|(filename, dispatch)| { | ||
match dispatch { | ||||
Dispatch::Bad(_) => return Some((filename, dispatch)), | ||||
_ => {} | ||||
}; | ||||
// TODO do this in //, not at the end | ||||
if !dmap.contains_key(filename.deref()) { | ||||
if (options.list_ignored || matcher.exact_match(&filename)) | ||||
&& dir_ignore_fn(&filename) | ||||
{ | ||||
if options.list_ignored { | ||||
return Some((filename.to_owned(), Dispatch::Ignored)); | ||||
} | ||||
} else { | ||||
if !ignore_fn(&filename) { | ||||
return Some((filename.to_owned(), Dispatch::Unknown)); | ||||
} | ||||
} | ||||
return None; | ||||
} | ||||
Some((filename, dispatch)) | ||||
}); | ||||
Ok((build_response(results), warnings)) | ||||
Raphaël Gomès
|
r43565 | } | ||