status.rs
342 lines
| 10.5 KiB
| application/rls-services+xml
|
RustLexer
Georges Racinet
|
r47578 | // status.rs | ||
// | ||||
// Copyright 2020, Georges Racinet <georges.racinets@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. | ||||
use crate::error::CommandError; | ||||
use crate::ui::Ui; | ||||
use clap::{Arg, SubCommand}; | ||||
use hg; | ||||
Simon Sapin
|
r48165 | use hg::dirstate_tree::dirstate_map::DirstateMap; | ||
Simon Sapin
|
r48474 | use hg::dirstate_tree::on_disk; | ||
Simon Sapin
|
r48113 | use hg::errors::HgResultExt; | ||
Georges Racinet
|
r47578 | use hg::errors::IoResultExt; | ||
use hg::matchers::AlwaysMatcher; | ||||
use hg::operations::cat; | ||||
use hg::repo::Repo; | ||||
use hg::revlog::node::Node; | ||||
use hg::utils::hg_path::{hg_path_to_os_string, HgPath}; | ||||
Simon Sapin
|
r48165 | use hg::StatusError; | ||
Georges Racinet
|
r47578 | use hg::{HgPathCow, StatusOptions}; | ||
use log::{info, warn}; | ||||
use std::convert::TryInto; | ||||
use std::fs; | ||||
use std::io::BufReader; | ||||
use std::io::Read; | ||||
pub const HELP_TEXT: &str = " | ||||
Show changed files in the working directory | ||||
This is a pure Rust version of `hg status`. | ||||
Some options might be missing, check the list below. | ||||
"; | ||||
pub fn args() -> clap::App<'static, 'static> { | ||||
SubCommand::with_name("status") | ||||
.alias("st") | ||||
.about(HELP_TEXT) | ||||
.arg( | ||||
Arg::with_name("all") | ||||
.help("show status of all files") | ||||
.short("-A") | ||||
.long("--all"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("modified") | ||||
.help("show only modified files") | ||||
.short("-m") | ||||
.long("--modified"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("added") | ||||
.help("show only added files") | ||||
.short("-a") | ||||
.long("--added"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("removed") | ||||
.help("show only removed files") | ||||
.short("-r") | ||||
.long("--removed"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("clean") | ||||
.help("show only clean files") | ||||
.short("-c") | ||||
.long("--clean"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("deleted") | ||||
.help("show only deleted files") | ||||
.short("-d") | ||||
.long("--deleted"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("unknown") | ||||
.help("show only unknown (not tracked) files") | ||||
.short("-u") | ||||
.long("--unknown"), | ||||
) | ||||
.arg( | ||||
Arg::with_name("ignored") | ||||
.help("show only ignored files") | ||||
.short("-i") | ||||
.long("--ignored"), | ||||
) | ||||
} | ||||
/// Pure data type allowing the caller to specify file states to display | ||||
#[derive(Copy, Clone, Debug)] | ||||
pub struct DisplayStates { | ||||
pub modified: bool, | ||||
pub added: bool, | ||||
pub removed: bool, | ||||
pub clean: bool, | ||||
pub deleted: bool, | ||||
pub unknown: bool, | ||||
pub ignored: bool, | ||||
} | ||||
pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates { | ||||
modified: true, | ||||
added: true, | ||||
removed: true, | ||||
clean: false, | ||||
deleted: true, | ||||
unknown: true, | ||||
ignored: false, | ||||
}; | ||||
pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates { | ||||
modified: true, | ||||
added: true, | ||||
removed: true, | ||||
clean: true, | ||||
deleted: true, | ||||
unknown: true, | ||||
ignored: true, | ||||
}; | ||||
impl DisplayStates { | ||||
pub fn is_empty(&self) -> bool { | ||||
!(self.modified | ||||
|| self.added | ||||
|| self.removed | ||||
|| self.clean | ||||
|| self.deleted | ||||
|| self.unknown | ||||
|| self.ignored) | ||||
} | ||||
} | ||||
pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { | ||||
let status_enabled_default = false; | ||||
let status_enabled = invocation.config.get_option(b"rhg", b"status")?; | ||||
if !status_enabled.unwrap_or(status_enabled_default) { | ||||
return Err(CommandError::unsupported( | ||||
"status is experimental in rhg (enable it with 'rhg.status = true' \ | ||||
or enable fallback with 'rhg.on-unsupported = fallback')" | ||||
)); | ||||
} | ||||
let ui = invocation.ui; | ||||
let args = invocation.subcommand_args; | ||||
let display_states = if args.is_present("all") { | ||||
// TODO when implementing `--quiet`: it excludes clean files | ||||
// from `--all` | ||||
ALL_DISPLAY_STATES | ||||
} else { | ||||
let requested = DisplayStates { | ||||
modified: args.is_present("modified"), | ||||
added: args.is_present("added"), | ||||
removed: args.is_present("removed"), | ||||
clean: args.is_present("clean"), | ||||
deleted: args.is_present("deleted"), | ||||
unknown: args.is_present("unknown"), | ||||
ignored: args.is_present("ignored"), | ||||
}; | ||||
if requested.is_empty() { | ||||
DEFAULT_DISPLAY_STATES | ||||
} else { | ||||
requested | ||||
} | ||||
}; | ||||
let repo = invocation.repo?; | ||||
Simon Sapin
|
r48474 | let dirstate_data_mmap; | ||
Simon Sapin
|
r48165 | let (mut dmap, parents) = if repo.has_dirstate_v2() { | ||
Simon Sapin
|
r48474 | let parents; | ||
let dirstate_data; | ||||
Simon Sapin
|
r48475 | let data_size; | ||
Simon Sapin
|
r48474 | if let Some(docket_data) = | ||
repo.hg_vfs().read("dirstate").io_not_found_as_none()? | ||||
{ | ||||
let docket = on_disk::read_docket(&docket_data)?; | ||||
parents = Some(docket.parents()); | ||||
Simon Sapin
|
r48475 | data_size = docket.data_size(); | ||
Simon Sapin
|
r48474 | dirstate_data_mmap = repo | ||
.hg_vfs() | ||||
.mmap_open(docket.data_filename()) | ||||
.io_not_found_as_none()?; | ||||
dirstate_data = dirstate_data_mmap.as_deref().unwrap_or(b""); | ||||
} else { | ||||
parents = None; | ||||
Simon Sapin
|
r48475 | data_size = 0; | ||
Simon Sapin
|
r48474 | dirstate_data = b""; | ||
} | ||||
Simon Sapin
|
r48475 | let dmap = DirstateMap::new_v2(dirstate_data, data_size)?; | ||
Simon Sapin
|
r48474 | (dmap, parents) | ||
Simon Sapin
|
r48165 | } else { | ||
Simon Sapin
|
r48474 | dirstate_data_mmap = | ||
repo.hg_vfs().mmap_open("dirstate").io_not_found_as_none()?; | ||||
let dirstate_data = dirstate_data_mmap.as_deref().unwrap_or(b""); | ||||
Simon Sapin
|
r48165 | DirstateMap::new_v1(dirstate_data)? | ||
}; | ||||
Simon Sapin
|
r48474 | |||
Georges Racinet
|
r47578 | let options = StatusOptions { | ||
// TODO should be provided by the dirstate parsing and | ||||
// hence be stored on dmap. Using a value that assumes we aren't | ||||
// below the time resolution granularity of the FS and the | ||||
// dirstate. | ||||
last_normal_time: 0, | ||||
// we're currently supporting file systems with exec flags only | ||||
// anyway | ||||
check_exec: true, | ||||
list_clean: display_states.clean, | ||||
list_unknown: display_states.unknown, | ||||
list_ignored: display_states.ignored, | ||||
collect_traversed_dirs: false, | ||||
}; | ||||
let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded | ||||
Simon Sapin
|
r48165 | let (mut ds_status, pattern_warnings) = hg::dirstate_tree::status::status( | ||
&mut dmap, | ||||
Georges Racinet
|
r47578 | &AlwaysMatcher, | ||
repo.working_directory_path().to_owned(), | ||||
vec![ignore_file], | ||||
options, | ||||
)?; | ||||
if !pattern_warnings.is_empty() { | ||||
warn!("Pattern warnings: {:?}", &pattern_warnings); | ||||
} | ||||
if !ds_status.bad.is_empty() { | ||||
warn!("Bad matches {:?}", &(ds_status.bad)) | ||||
} | ||||
Simon Sapin
|
r47880 | if !ds_status.unsure.is_empty() { | ||
Georges Racinet
|
r47578 | info!( | ||
"Files to be rechecked by retrieval from filelog: {:?}", | ||||
Simon Sapin
|
r47880 | &ds_status.unsure | ||
Georges Racinet
|
r47578 | ); | ||
} | ||||
Simon Sapin
|
r48112 | if !ds_status.unsure.is_empty() | ||
&& (display_states.modified || display_states.clean) | ||||
{ | ||||
Georges Racinet
|
r47578 | let p1: Node = parents | ||
.expect( | ||||
"Dirstate with no parents should not list any file to | ||||
Simon Sapin
|
r48112 | be rechecked for modifications", | ||
Georges Racinet
|
r47578 | ) | ||
.p1 | ||||
.into(); | ||||
let p1_hex = format!("{:x}", p1); | ||||
Simon Sapin
|
r47880 | for to_check in ds_status.unsure { | ||
Georges Racinet
|
r47578 | if cat_file_is_modified(repo, &to_check, &p1_hex)? { | ||
Simon Sapin
|
r48112 | if display_states.modified { | ||
ds_status.modified.push(to_check); | ||||
} | ||||
Georges Racinet
|
r47578 | } else { | ||
Simon Sapin
|
r48112 | if display_states.clean { | ||
ds_status.clean.push(to_check); | ||||
} | ||||
Georges Racinet
|
r47578 | } | ||
} | ||||
Simon Sapin
|
r48112 | } | ||
if display_states.modified { | ||||
display_status_paths(ui, &mut ds_status.modified, b"M")?; | ||||
Georges Racinet
|
r47578 | } | ||
if display_states.added { | ||||
Simon Sapin
|
r48112 | display_status_paths(ui, &mut ds_status.added, b"A")?; | ||
Georges Racinet
|
r47578 | } | ||
if display_states.removed { | ||||
Simon Sapin
|
r48112 | display_status_paths(ui, &mut ds_status.removed, b"R")?; | ||
Georges Racinet
|
r47578 | } | ||
if display_states.deleted { | ||||
Simon Sapin
|
r48112 | display_status_paths(ui, &mut ds_status.deleted, b"!")?; | ||
Georges Racinet
|
r47578 | } | ||
if display_states.unknown { | ||||
Simon Sapin
|
r48112 | display_status_paths(ui, &mut ds_status.unknown, b"?")?; | ||
Georges Racinet
|
r47578 | } | ||
if display_states.ignored { | ||||
Simon Sapin
|
r48112 | display_status_paths(ui, &mut ds_status.ignored, b"I")?; | ||
} | ||||
if display_states.clean { | ||||
display_status_paths(ui, &mut ds_status.clean, b"C")?; | ||||
Georges Racinet
|
r47578 | } | ||
Ok(()) | ||||
} | ||||
// Probably more elegant to use a Deref or Borrow trait rather than | ||||
// harcode HgPathBuf, but probably not really useful at this point | ||||
fn display_status_paths( | ||||
ui: &Ui, | ||||
Simon Sapin
|
r48112 | paths: &mut [HgPathCow], | ||
Georges Racinet
|
r47578 | status_prefix: &[u8], | ||
) -> Result<(), CommandError> { | ||||
Simon Sapin
|
r48112 | paths.sort_unstable(); | ||
Georges Racinet
|
r47578 | for path in paths { | ||
// Same TODO as in commands::root | ||||
let bytes: &[u8] = path.as_bytes(); | ||||
// TODO optim, probably lots of unneeded copies here, especially | ||||
// if out stream is buffered | ||||
ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?; | ||||
} | ||||
Ok(()) | ||||
} | ||||
/// Check if a file is modified by comparing actual repo store and file system. | ||||
/// | ||||
/// This meant to be used for those that the dirstate cannot resolve, due | ||||
/// to time resolution limits. | ||||
/// | ||||
/// TODO: detect permission bits and similar metadata modifications | ||||
fn cat_file_is_modified( | ||||
repo: &Repo, | ||||
hg_path: &HgPath, | ||||
rev: &str, | ||||
) -> Result<bool, CommandError> { | ||||
// TODO CatRev expects &[HgPathBuf], something like | ||||
// &[impl Deref<HgPath>] would be nicer and should avoid the copy | ||||
let path_bufs = [hg_path.into()]; | ||||
// TODO IIUC CatRev returns a simple Vec<u8> for all files | ||||
// being able to tell them apart as (path, bytes) would be nicer | ||||
// and OPTIM would allow manifest resolution just once. | ||||
let output = cat(repo, rev, &path_bufs).map_err(|e| (e, rev))?; | ||||
let fs_path = repo | ||||
.working_directory_vfs() | ||||
.join(hg_path_to_os_string(hg_path).expect("HgPath conversion")); | ||||
let hg_data_len: u64 = match output.concatenated.len().try_into() { | ||||
Ok(v) => v, | ||||
Err(_) => { | ||||
// conversion of data length to u64 failed, | ||||
// good luck for any file to have this content | ||||
return Ok(true); | ||||
} | ||||
}; | ||||
let fobj = fs::File::open(&fs_path).when_reading_file(&fs_path)?; | ||||
if fobj.metadata().map_err(|e| StatusError::from(e))?.len() != hg_data_len | ||||
{ | ||||
return Ok(true); | ||||
} | ||||
for (fs_byte, hg_byte) in | ||||
BufReader::new(fobj).bytes().zip(output.concatenated) | ||||
{ | ||||
if fs_byte.map_err(|e| StatusError::from(e))? != hg_byte { | ||||
return Ok(true); | ||||
} | ||||
} | ||||
Ok(false) | ||||
} | ||||