|
|
// 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 crate::utils::path_utils::relativize_paths;
|
|
|
use clap::{Arg, SubCommand};
|
|
|
use format_bytes::format_bytes;
|
|
|
use hg;
|
|
|
use hg::config::Config;
|
|
|
use hg::dirstate::has_exec_bit;
|
|
|
use hg::errors::HgError;
|
|
|
use hg::manifest::Manifest;
|
|
|
use hg::matchers::AlwaysMatcher;
|
|
|
use hg::repo::Repo;
|
|
|
use hg::utils::files::get_bytes_from_os_string;
|
|
|
use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
|
|
|
use hg::{HgPathCow, StatusOptions};
|
|
|
use log::{info, warn};
|
|
|
|
|
|
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"),
|
|
|
)
|
|
|
.arg(
|
|
|
Arg::with_name("no-status")
|
|
|
.help("hide status prefix")
|
|
|
.short("-n")
|
|
|
.long("--no-status"),
|
|
|
)
|
|
|
}
|
|
|
|
|
|
/// 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')"
|
|
|
));
|
|
|
}
|
|
|
|
|
|
// TODO: lift these limitations
|
|
|
if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
|
|
|
return Err(CommandError::unsupported(
|
|
|
"ui.tweakdefaults is not yet supported with rhg status",
|
|
|
));
|
|
|
}
|
|
|
if invocation.config.get_bool(b"ui", b"statuscopies")? {
|
|
|
return Err(CommandError::unsupported(
|
|
|
"ui.statuscopies is not yet supported with rhg status",
|
|
|
));
|
|
|
}
|
|
|
if invocation
|
|
|
.config
|
|
|
.get(b"commands", b"status.terse")
|
|
|
.is_some()
|
|
|
{
|
|
|
return Err(CommandError::unsupported(
|
|
|
"status.terse is not yet supported with rhg status",
|
|
|
));
|
|
|
}
|
|
|
|
|
|
let ui = invocation.ui;
|
|
|
let config = invocation.config;
|
|
|
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 no_status = args.is_present("no-status");
|
|
|
|
|
|
let repo = invocation.repo?;
|
|
|
let mut dmap = repo.dirstate_map_mut()?;
|
|
|
|
|
|
let options = StatusOptions {
|
|
|
// 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
|
|
|
let (mut ds_status, pattern_warnings) = dmap.status(
|
|
|
&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))
|
|
|
}
|
|
|
if !ds_status.unsure.is_empty() {
|
|
|
info!(
|
|
|
"Files to be rechecked by retrieval from filelog: {:?}",
|
|
|
&ds_status.unsure
|
|
|
);
|
|
|
}
|
|
|
if !ds_status.unsure.is_empty()
|
|
|
&& (display_states.modified || display_states.clean)
|
|
|
{
|
|
|
let p1 = repo.dirstate_parents()?.p1;
|
|
|
let manifest = repo.manifest_for_node(p1).map_err(|e| {
|
|
|
CommandError::from((e, &*format!("{:x}", p1.short())))
|
|
|
})?;
|
|
|
for to_check in ds_status.unsure {
|
|
|
if unsure_is_modified(repo, &manifest, &to_check)? {
|
|
|
if display_states.modified {
|
|
|
ds_status.modified.push(to_check);
|
|
|
}
|
|
|
} else {
|
|
|
if display_states.clean {
|
|
|
ds_status.clean.push(to_check);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if display_states.modified {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.modified,
|
|
|
b"M",
|
|
|
)?;
|
|
|
}
|
|
|
if display_states.added {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.added,
|
|
|
b"A",
|
|
|
)?;
|
|
|
}
|
|
|
if display_states.removed {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.removed,
|
|
|
b"R",
|
|
|
)?;
|
|
|
}
|
|
|
if display_states.deleted {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.deleted,
|
|
|
b"!",
|
|
|
)?;
|
|
|
}
|
|
|
if display_states.unknown {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.unknown,
|
|
|
b"?",
|
|
|
)?;
|
|
|
}
|
|
|
if display_states.ignored {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.ignored,
|
|
|
b"I",
|
|
|
)?;
|
|
|
}
|
|
|
if display_states.clean {
|
|
|
display_status_paths(
|
|
|
ui,
|
|
|
repo,
|
|
|
config,
|
|
|
no_status,
|
|
|
&mut ds_status.clean,
|
|
|
b"C",
|
|
|
)?;
|
|
|
}
|
|
|
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,
|
|
|
repo: &Repo,
|
|
|
config: &Config,
|
|
|
no_status: bool,
|
|
|
paths: &mut [HgPathCow],
|
|
|
status_prefix: &[u8],
|
|
|
) -> Result<(), CommandError> {
|
|
|
paths.sort_unstable();
|
|
|
let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
|
|
|
relative = config
|
|
|
.get_option(b"commands", b"status.relative")?
|
|
|
.unwrap_or(relative);
|
|
|
let print_path = |path: &[u8]| {
|
|
|
// TODO optim, probably lots of unneeded copies here, especially
|
|
|
// if out stream is buffered
|
|
|
if no_status {
|
|
|
ui.write_stdout(&format_bytes!(b"{}\n", path))
|
|
|
} else {
|
|
|
ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
|
|
|
}
|
|
|
};
|
|
|
|
|
|
if relative && !ui.plain() {
|
|
|
relativize_paths(repo, paths.iter().map(Ok), |path| {
|
|
|
print_path(&path)
|
|
|
})?;
|
|
|
} else {
|
|
|
for path in paths {
|
|
|
print_path(path.as_bytes())?
|
|
|
}
|
|
|
}
|
|
|
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.
|
|
|
fn unsure_is_modified(
|
|
|
repo: &Repo,
|
|
|
manifest: &Manifest,
|
|
|
hg_path: &HgPath,
|
|
|
) -> Result<bool, HgError> {
|
|
|
let vfs = repo.working_directory_vfs();
|
|
|
let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
|
|
|
let fs_metadata = vfs.symlink_metadata(&fs_path)?;
|
|
|
let is_symlink = fs_metadata.file_type().is_symlink();
|
|
|
// TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
|
|
|
// dirstate
|
|
|
let fs_flags = if is_symlink {
|
|
|
Some(b'l')
|
|
|
} else if has_exec_bit(&fs_metadata) {
|
|
|
Some(b'x')
|
|
|
} else {
|
|
|
None
|
|
|
};
|
|
|
|
|
|
let entry = manifest
|
|
|
.find_file(hg_path)?
|
|
|
.expect("ambgious file not in p1");
|
|
|
if entry.flags != fs_flags {
|
|
|
return Ok(true);
|
|
|
}
|
|
|
let filelog = repo.filelog(hg_path)?;
|
|
|
let filelog_entry =
|
|
|
filelog.data_for_node(entry.node_id()?).map_err(|_| {
|
|
|
HgError::corrupted("filelog missing node from manifest")
|
|
|
})?;
|
|
|
let contents_in_p1 = filelog_entry.data()?;
|
|
|
|
|
|
let fs_contents = if is_symlink {
|
|
|
get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
|
|
|
} else {
|
|
|
vfs.read(fs_path)?
|
|
|
};
|
|
|
return Ok(contents_in_p1 != &*fs_contents);
|
|
|
}
|
|
|
|