status.rs
321 lines
| 10.3 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
|
r44367 | matchers::Matcher, | ||
Raphaël Gomès
|
r44003 | utils::{ | ||
files::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
|
r44003 | }, | ||
CopyMap, DirstateEntry, DirstateMap, EntryState, | ||||
}; | ||||
Raphaël Gomès
|
r43565 | use rayon::prelude::*; | ||
Raphaël Gomès
|
r44367 | use std::collections::HashSet; | ||
Raphaël Gomès
|
r45010 | use std::fs::{read_dir, DirEntry}; | ||
Raphaël Gomès
|
r43565 | use std::path::Path; | ||
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. | ||||
enum Dispatch { | ||||
Unsure, | ||||
Modified, | ||||
Added, | ||||
Removed, | ||||
Deleted, | ||||
Clean, | ||||
Unknown, | ||||
} | ||||
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 { | ||||
Dispatch::Unknown | ||||
} | ||||
} | ||||
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
|
r44367 | /// Get stat data about the files explicitly specified by match. | ||
/// TODO subrepos | ||||
fn walk_explicit<'a>( | ||||
files: &'a HashSet<&HgPath>, | ||||
dmap: &'a DirstateMap, | ||||
root_dir: impl AsRef<Path> + Sync + Send, | ||||
Raphaël Gomès
|
r45011 | options: StatusOptions, | ||
Raphaël Gomès
|
r44367 | ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> { | ||
files.par_iter().filter_map(move |filename| { | ||||
// TODO normalization | ||||
let normalized = filename.as_ref(); | ||||
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(); | ||||
match st { | ||||
Ok(meta) => { | ||||
let file_type = meta.file_type(); | ||||
if file_type.is_file() || file_type.is_symlink() { | ||||
if let Some(entry) = dmap.get(normalized) { | ||||
return Some(Ok(( | ||||
normalized, | ||||
dispatch_found( | ||||
&normalized, | ||||
*entry, | ||||
HgMetadata::from_metadata(meta), | ||||
&dmap.copy_map, | ||||
Raphaël Gomès
|
r45011 | options, | ||
Raphaël Gomès
|
r44367 | ), | ||
))); | ||||
} | ||||
} else { | ||||
if dmap.contains_key(normalized) { | ||||
return Some(Ok((normalized, Dispatch::Removed))); | ||||
} | ||||
} | ||||
} | ||||
Err(_) => { | ||||
if let Some(entry) = dmap.get(normalized) { | ||||
return Some(Ok(( | ||||
normalized, | ||||
dispatch_missing(entry.state), | ||||
))); | ||||
} | ||||
} | ||||
}; | ||||
None | ||||
}) | ||||
} | ||||
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
|
r44000 | /// Stat all entries in the `DirstateMap` and mark them for dispatch into | ||
/// the relevant collections. | ||||
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) | ||||
if e.kind() == std::io::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), | ||||
} | ||||
}) | ||||
Raphaël Gomès
|
r43565 | } | ||
Raphaël Gomès
|
r44000 | pub struct StatusResult<'a> { | ||
pub modified: Vec<&'a HgPath>, | ||||
pub added: Vec<&'a HgPath>, | ||||
pub removed: Vec<&'a HgPath>, | ||||
pub deleted: Vec<&'a HgPath>, | ||||
pub clean: Vec<&'a HgPath>, | ||||
Gregory Szorc
|
r44270 | /* TODO ignored | ||
* TODO unknown */ | ||||
Raphaël Gomès
|
r43565 | } | ||
Raphaël Gomès
|
r44367 | fn build_response<'a>( | ||
results: impl IntoIterator<Item = IoResult<(&'a HgPath, Dispatch)>>, | ||||
) -> IoResult<(Vec<&'a HgPath>, StatusResult<'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
|
r44367 | for res in results.into_iter() { | ||
let (filename, dispatch) = res?; | ||||
Raphaël Gomès
|
r44000 | match dispatch { | ||
Dispatch::Unknown => {} | ||||
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
|
r43565 | } | ||
} | ||||
Raphaël Gomès
|
r44367 | Ok(( | ||
Raphaël Gomès
|
r43565 | lookup, | ||
StatusResult { | ||||
modified, | ||||
added, | ||||
removed, | ||||
deleted, | ||||
clean, | ||||
}, | ||||
Raphaël Gomès
|
r44367 | )) | ||
Raphaël Gomès
|
r43565 | } | ||
Raphaël Gomès
|
r44367 | pub fn status<'a: 'c, 'b: 'c, 'c>( | ||
dmap: &'a DirstateMap, | ||||
Martin von Zweigbergk
|
r44654 | matcher: &'b impl Matcher, | ||
Raphaël Gomès
|
r44001 | root_dir: impl AsRef<Path> + Sync + Send + Copy, | ||
Raphaël Gomès
|
r45011 | options: StatusOptions, | ||
Raphaël Gomès
|
r44367 | ) -> IoResult<(Vec<&'c HgPath>, StatusResult<'c>)> { | ||
let files = matcher.file_set(); | ||||
let mut results = vec![]; | ||||
if let Some(files) = files { | ||||
Raphaël Gomès
|
r45011 | results.par_extend(walk_explicit(&files, &dmap, root_dir, options)); | ||
Raphaël Gomès
|
r44367 | } | ||
Raphaël Gomès
|
r44000 | |||
Raphaël Gomès
|
r44367 | if !matcher.is_exact() { | ||
Raphaël Gomès
|
r45011 | let stat_results = stat_dmap_entries(&dmap, root_dir, options); | ||
Raphaël Gomès
|
r44367 | results.par_extend(stat_results); | ||
} | ||||
build_response(results) | ||||
Raphaël Gomès
|
r43565 | } | ||