main.rs
825 lines
| 27.3 KiB
| application/rls-services+xml
|
RustLexer
Antoine Cezar
|
r46101 | extern crate log; | ||
Simon Sapin
|
r49159 | use crate::error::CommandError; | ||
Raphaël Gomès
|
r49622 | use crate::ui::{local_to_utf8, Ui}; | ||
Raphaël Gomès
|
r50534 | use clap::{command, Arg, ArgMatches}; | ||
Simon Sapin
|
r47467 | use format_bytes::{format_bytes, join}; | ||
Arseniy Alekseyev
|
r50407 | use hg::config::{Config, ConfigSource, PlainInfo}; | ||
Simon Sapin
|
r47335 | use hg::repo::{Repo, RepoError}; | ||
Simon Sapin
|
r47423 | use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; | ||
use hg::utils::SliceExt; | ||||
r50087 | use hg::{exit_codes, requirements}; | |||
Arseniy Alekseyev
|
r50407 | use std::borrow::Cow; | ||
Raphaël Gomès
|
r49270 | use std::collections::HashSet; | ||
Simon Sapin
|
r47423 | use std::ffi::OsString; | ||
Raphaël Gomès
|
r50043 | use std::os::unix::prelude::CommandExt; | ||
Simon Sapin
|
r47423 | use std::path::PathBuf; | ||
Simon Sapin
|
r47425 | use std::process::Command; | ||
Antoine Cezar
|
r45593 | |||
Simon Sapin
|
r47343 | mod blackbox; | ||
Simon Sapin
|
r49584 | mod color; | ||
Antoine Cezar
|
r45515 | mod error; | ||
Antoine Cezar
|
r45592 | mod ui; | ||
Pulkit Goyal
|
r48988 | pub mod utils { | ||
pub mod path_utils; | ||||
} | ||||
Antoine Cezar
|
r45503 | |||
Simon Sapin
|
r47343 | fn main_with_result( | ||
Arseniy Alekseyev
|
r49961 | argv: Vec<OsString>, | ||
Simon Sapin
|
r47423 | process_start_time: &blackbox::ProcessStartTime, | ||
Simon Sapin
|
r47343 | ui: &ui::Ui, | ||
Simon Sapin
|
r47423 | repo: Result<&Repo, &NoRepoInCwdError>, | ||
config: &Config, | ||||
Simon Sapin
|
r47343 | ) -> Result<(), CommandError> { | ||
Simon Sapin
|
r49585 | check_unsupported(config, repo)?; | ||
Simon Sapin
|
r47467 | |||
Raphaël Gomès
|
r50534 | let app = command!() | ||
.subcommand_required(true) | ||||
Simon Sapin
|
r47351 | .arg( | ||
Raphaël Gomès
|
r50534 | Arg::new("repository") | ||
Simon Sapin
|
r47351 | .help("repository root directory") | ||
Raphaël Gomès
|
r50534 | .short('R') | ||
Simon Sapin
|
r47351 | .value_name("REPO") | ||
// Both ok: `hg -R ./foo log` or `hg log -R ./foo` | ||||
.global(true), | ||||
) | ||||
.arg( | ||||
Raphaël Gomès
|
r50534 | Arg::new("config") | ||
Simon Sapin
|
r47351 | .help("set/override config option (use 'section.name=value')") | ||
.value_name("CONFIG") | ||||
.global(true) | ||||
Raphaël Gomès
|
r50534 | .long("config") | ||
Simon Sapin
|
r47351 | // Ok: `--config section.key1=val --config section.key2=val2` | ||
// Not ok: `--config section.key1=val section.key2=val2` | ||||
Raphaël Gomès
|
r50534 | .action(clap::ArgAction::Append), | ||
Simon Sapin
|
r47351 | ) | ||
Simon Sapin
|
r47470 | .arg( | ||
Raphaël Gomès
|
r50534 | Arg::new("cwd") | ||
Simon Sapin
|
r47470 | .help("change working directory") | ||
.value_name("DIR") | ||||
Raphaël Gomès
|
r50534 | .long("cwd") | ||
Simon Sapin
|
r47470 | .global(true), | ||
) | ||||
Simon Sapin
|
r49583 | .arg( | ||
Raphaël Gomès
|
r50534 | Arg::new("color") | ||
Simon Sapin
|
r49583 | .help("when to colorize (boolean, always, auto, never, or debug)") | ||
.value_name("TYPE") | ||||
Raphaël Gomès
|
r50534 | .long("color") | ||
Simon Sapin
|
r49583 | .global(true), | ||
) | ||||
Simon Sapin
|
r47252 | .version("0.0.1"); | ||
let app = add_subcommand_args(app); | ||||
Antoine Cezar
|
r45593 | |||
Raphaël Gomès
|
r50809 | let matches = app.try_get_matches_from(argv.iter())?; | ||
Simon Sapin
|
r47253 | |||
Raphaël Gomès
|
r50534 | let (subcommand_name, subcommand_args) = | ||
matches.subcommand().expect("subcommand required"); | ||||
Raphaël Gomès
|
r48889 | |||
Raphaël Gomès
|
r48890 | // Mercurial allows users to define "defaults" for commands, fallback | ||
// if a default is detected for the current command | ||||
let defaults = config.get_str(b"defaults", subcommand_name.as_bytes()); | ||||
if defaults?.is_some() { | ||||
let msg = "`defaults` config set"; | ||||
return Err(CommandError::unsupported(msg)); | ||||
} | ||||
Raphaël Gomès
|
r48889 | for prefix in ["pre", "post", "fail"].iter() { | ||
// Mercurial allows users to define generic hooks for commands, | ||||
// fallback if any are detected | ||||
let item = format!("{}-{}", prefix, subcommand_name); | ||||
let hook_for_command = config.get_str(b"hooks", item.as_bytes())?; | ||||
if hook_for_command.is_some() { | ||||
let msg = format!("{}-{} hook defined", prefix, subcommand_name); | ||||
return Err(CommandError::unsupported(msg)); | ||||
} | ||||
} | ||||
Simon Sapin
|
r47252 | let run = subcommand_run_fn(subcommand_name) | ||
Raphaël Gomès
|
r50534 | .expect("unknown subcommand name from clap despite Command::subcommand_required"); | ||
Antoine Cezar
|
r45593 | |||
Simon Sapin
|
r47343 | let invocation = CliInvocation { | ||
Simon Sapin
|
r47334 | ui, | ||
subcommand_args, | ||||
Simon Sapin
|
r47423 | config, | ||
repo, | ||||
Simon Sapin
|
r47343 | }; | ||
Raphaël Gomès
|
r48891 | |||
if let Ok(repo) = repo { | ||||
// We don't support subrepos, fallback if the subrepos file is present | ||||
if repo.working_directory_vfs().join(".hgsub").exists() { | ||||
let msg = "subrepos (.hgsub is present)"; | ||||
return Err(CommandError::unsupported(msg)); | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r49243 | if config.is_extension_enabled(b"blackbox") { | ||
let blackbox = | ||||
blackbox::Blackbox::new(&invocation, process_start_time)?; | ||||
Arseniy Alekseyev
|
r49961 | blackbox.log_command_start(argv.iter()); | ||
Raphaël Gomès
|
r49243 | let result = run(&invocation); | ||
Arseniy Alekseyev
|
r49961 | blackbox.log_command_end( | ||
argv.iter(), | ||||
exit_code( | ||||
&result, | ||||
// TODO: show a warning or combine with original error if | ||||
// `get_bool` returns an error | ||||
config | ||||
.get_bool(b"ui", b"detailed-exit-code") | ||||
.unwrap_or(false), | ||||
), | ||||
); | ||||
Raphaël Gomès
|
r49243 | result | ||
} else { | ||||
run(&invocation) | ||||
} | ||||
Simon Sapin
|
r47333 | } | ||
Simon Sapin
|
r47252 | |||
Arseniy Alekseyev
|
r49961 | fn rhg_main(argv: Vec<OsString>) -> ! { | ||
Simon Sapin
|
r47343 | // Run this first, before we find out if the blackbox extension is even | ||
// enabled, in order to include everything in-between in the duration | ||||
// measurements. Reading config files can be slow if they’re on NFS. | ||||
let process_start_time = blackbox::ProcessStartTime::now(); | ||||
Simon Sapin
|
r47423 | env_logger::init(); | ||
Simon Sapin
|
r47333 | |||
Arseniy Alekseyev
|
r49961 | let early_args = EarlyArgs::parse(&argv); | ||
Simon Sapin
|
r47470 | |||
let initial_current_dir = early_args.cwd.map(|cwd| { | ||||
let cwd = get_path_from_bytes(&cwd); | ||||
std::env::current_dir() | ||||
.and_then(|initial| { | ||||
std::env::set_current_dir(cwd)?; | ||||
Ok(initial) | ||||
}) | ||||
.unwrap_or_else(|error| { | ||||
exit( | ||||
Arseniy Alekseyev
|
r49961 | &argv, | ||
Simon Sapin
|
r47470 | &None, | ||
Simon Sapin
|
r49582 | &Ui::new_infallible(&Config::empty()), | ||
Simon Sapin
|
r47470 | OnUnsupported::Abort, | ||
Err(CommandError::abort(format!( | ||||
"abort: {}: '{}'", | ||||
error, | ||||
cwd.display() | ||||
))), | ||||
Pulkit Goyal
|
r47576 | false, | ||
Simon Sapin
|
r47470 | ) | ||
}) | ||||
}); | ||||
Pulkit Goyal
|
r48198 | let mut non_repo_config = | ||
Config::load_non_repo().unwrap_or_else(|error| { | ||||
Simon Sapin
|
r47424 | // Normally this is decided based on config, but we don’t have that | ||
// available. As of this writing config loading never returns an | ||||
// "unsupported" error but that is not enforced by the type system. | ||||
let on_unsupported = OnUnsupported::Abort; | ||||
Pulkit Goyal
|
r47576 | exit( | ||
Arseniy Alekseyev
|
r49961 | &argv, | ||
Pulkit Goyal
|
r47576 | &initial_current_dir, | ||
Simon Sapin
|
r49582 | &Ui::new_infallible(&Config::empty()), | ||
Pulkit Goyal
|
r47576 | on_unsupported, | ||
Err(error.into()), | ||||
false, | ||||
) | ||||
Simon Sapin
|
r47424 | }); | ||
Simon Sapin
|
r47423 | |||
Pulkit Goyal
|
r48198 | non_repo_config | ||
Simon Sapin
|
r49583 | .load_cli_args(early_args.config, early_args.color) | ||
Pulkit Goyal
|
r48198 | .unwrap_or_else(|error| { | ||
exit( | ||||
Arseniy Alekseyev
|
r49961 | &argv, | ||
Pulkit Goyal
|
r48198 | &initial_current_dir, | ||
Simon Sapin
|
r49582 | &Ui::new_infallible(&non_repo_config), | ||
Arseniy Alekseyev
|
r49176 | OnUnsupported::from_config(&non_repo_config), | ||
Pulkit Goyal
|
r48198 | Err(error.into()), | ||
non_repo_config | ||||
.get_bool(b"ui", b"detailed-exit-code") | ||||
.unwrap_or(false), | ||||
) | ||||
}); | ||||
Simon Sapin
|
r47463 | if let Some(repo_path_bytes) = &early_args.repo { | ||
lazy_static::lazy_static! { | ||||
static ref SCHEME_RE: regex::bytes::Regex = | ||||
// Same as `_matchscheme` in `mercurial/util.py` | ||||
regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); | ||||
} | ||||
Raphaël Gomès
|
r50809 | if SCHEME_RE.is_match(repo_path_bytes) { | ||
Simon Sapin
|
r47463 | exit( | ||
Arseniy Alekseyev
|
r49961 | &argv, | ||
Simon Sapin
|
r47470 | &initial_current_dir, | ||
Simon Sapin
|
r49582 | &Ui::new_infallible(&non_repo_config), | ||
Arseniy Alekseyev
|
r49176 | OnUnsupported::from_config(&non_repo_config), | ||
Simon Sapin
|
r47463 | Err(CommandError::UnsupportedFeature { | ||
message: format_bytes!( | ||||
b"URL-like --repository {}", | ||||
repo_path_bytes | ||||
), | ||||
}), | ||||
Pulkit Goyal
|
r47576 | // TODO: show a warning or combine with original error if | ||
// `get_bool` returns an error | ||||
non_repo_config | ||||
.get_bool(b"ui", b"detailed-exit-code") | ||||
.unwrap_or(false), | ||||
Simon Sapin
|
r47463 | ) | ||
} | ||||
} | ||||
Raphaël Gomès
|
r50809 | let repo_arg = early_args.repo.unwrap_or_default(); | ||
Pulkit Goyal
|
r48196 | let repo_path: Option<PathBuf> = { | ||
if repo_arg.is_empty() { | ||||
None | ||||
} else { | ||||
let local_config = { | ||||
if std::env::var_os("HGRCSKIPREPO").is_none() { | ||||
Pulkit Goyal
|
r48197 | // TODO: handle errors from find_repo_root | ||
if let Ok(current_dir_path) = Repo::find_repo_root() { | ||||
Pulkit Goyal
|
r48196 | let config_files = vec![ | ||
ConfigSource::AbsPath( | ||||
current_dir_path.join(".hg/hgrc"), | ||||
), | ||||
ConfigSource::AbsPath( | ||||
current_dir_path.join(".hg/hgrc-not-shared"), | ||||
), | ||||
]; | ||||
// TODO: handle errors from | ||||
// `load_from_explicit_sources` | ||||
Config::load_from_explicit_sources(config_files).ok() | ||||
} else { | ||||
None | ||||
} | ||||
} else { | ||||
None | ||||
} | ||||
}; | ||||
let non_repo_config_val = { | ||||
let non_repo_val = non_repo_config.get(b"paths", &repo_arg); | ||||
match &non_repo_val { | ||||
Raphaël Gomès
|
r50809 | Some(val) if !val.is_empty() => home::home_dir() | ||
Pulkit Goyal
|
r48196 | .unwrap_or_else(|| PathBuf::from("~")) | ||
.join(get_path_from_bytes(val)) | ||||
.canonicalize() | ||||
// TODO: handle error and make it similar to python | ||||
// implementation maybe? | ||||
.ok(), | ||||
_ => None, | ||||
} | ||||
}; | ||||
let config_val = match &local_config { | ||||
None => non_repo_config_val, | ||||
Some(val) => { | ||||
let local_config_val = val.get(b"paths", &repo_arg); | ||||
match &local_config_val { | ||||
Raphaël Gomès
|
r50809 | Some(val) if !val.is_empty() => { | ||
Pulkit Goyal
|
r48196 | // presence of a local_config assures that | ||
// current_dir | ||||
// wont result in an Error | ||||
let canpath = hg::utils::current_dir() | ||||
.unwrap() | ||||
.join(get_path_from_bytes(val)) | ||||
.canonicalize(); | ||||
canpath.ok().or(non_repo_config_val) | ||||
} | ||||
_ => non_repo_config_val, | ||||
} | ||||
} | ||||
}; | ||||
Raphaël Gomès
|
r50826 | config_val | ||
.or_else(|| Some(get_path_from_bytes(&repo_arg).to_path_buf())) | ||||
Pulkit Goyal
|
r48196 | } | ||
}; | ||||
Raphaël Gomès
|
r50463 | let simple_exit = | ||
Arseniy Alekseyev
|
r50410 | |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! { | ||
exit( | ||||
&argv, | ||||
&initial_current_dir, | ||||
ui, | ||||
OnUnsupported::from_config(config), | ||||
result, | ||||
// TODO: show a warning or combine with original error if | ||||
// `get_bool` returns an error | ||||
non_repo_config | ||||
.get_bool(b"ui", b"detailed-exit-code") | ||||
.unwrap_or(false), | ||||
) | ||||
}; | ||||
let early_exit = |config: &Config, error: CommandError| -> ! { | ||||
Raphaël Gomès
|
r50809 | simple_exit(&Ui::new_infallible(config), config, Err(error)) | ||
Arseniy Alekseyev
|
r50410 | }; | ||
Pulkit Goyal
|
r48196 | let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned()) | ||
{ | ||||
Simon Sapin
|
r47423 | Ok(repo) => Ok(repo), | ||
Err(RepoError::NotFound { at }) if repo_path.is_none() => { | ||||
// Not finding a repo is not fatal yet, if `-R` was not given | ||||
Err(NoRepoInCwdError { cwd: at }) | ||||
Simon Sapin
|
r47343 | } | ||
Arseniy Alekseyev
|
r50410 | Err(error) => early_exit(&non_repo_config, error.into()), | ||
Simon Sapin
|
r47423 | }; | ||
let config = if let Ok(repo) = &repo_result { | ||||
repo.config() | ||||
} else { | ||||
&non_repo_config | ||||
}; | ||||
Arseniy Alekseyev
|
r50407 | |||
let mut config_cow = Cow::Borrowed(config); | ||||
Arseniy Alekseyev
|
r50408 | config_cow.to_mut().apply_plain(PlainInfo::from_env()); | ||
Arseniy Alekseyev
|
r50409 | if !ui::plain(Some("tweakdefaults")) | ||
&& config_cow | ||||
.as_ref() | ||||
.get_bool(b"ui", b"tweakdefaults") | ||||
Raphaël Gomès
|
r50809 | .unwrap_or_else(|error| early_exit(config, error.into())) | ||
Arseniy Alekseyev
|
r50409 | { | ||
config_cow.to_mut().tweakdefaults() | ||||
}; | ||||
Arseniy Alekseyev
|
r50407 | let config = config_cow.as_ref(); | ||
Raphaël Gomès
|
r50809 | let ui = Ui::new(config) | ||
.unwrap_or_else(|error| early_exit(config, error.into())); | ||||
Raphaël Gomès
|
r50464 | |||
if let Ok(true) = config.get_bool(b"rhg", b"fallback-immediately") { | ||||
exit( | ||||
&argv, | ||||
&initial_current_dir, | ||||
&ui, | ||||
OnUnsupported::Fallback { | ||||
executable: config | ||||
.get(b"rhg", b"fallback-executable") | ||||
.map(ToOwned::to_owned), | ||||
}, | ||||
Err(CommandError::unsupported( | ||||
"`rhg.fallback-immediately is true`", | ||||
)), | ||||
false, | ||||
) | ||||
} | ||||
Simon Sapin
|
r47423 | let result = main_with_result( | ||
Arseniy Alekseyev
|
r49961 | argv.iter().map(|s| s.to_owned()).collect(), | ||
Simon Sapin
|
r47423 | &process_start_time, | ||
&ui, | ||||
repo_result.as_ref(), | ||||
config, | ||||
); | ||||
Raphaël Gomès
|
r50809 | simple_exit(&ui, config, result) | ||
Simon Sapin
|
r47343 | } | ||
Arseniy Alekseyev
|
r49961 | fn main() -> ! { | ||
rhg_main(std::env::args_os().collect()) | ||||
} | ||||
Pulkit Goyal
|
r47576 | fn exit_code( | ||
result: &Result<(), CommandError>, | ||||
use_detailed_exit_code: bool, | ||||
) -> i32 { | ||||
Simon Sapin
|
r47343 | match result { | ||
Pulkit Goyal
|
r48199 | Ok(()) => exit_codes::OK, | ||
Pulkit Goyal
|
r47576 | Err(CommandError::Abort { | ||
Raphaël Gomès
|
r50382 | detailed_exit_code, .. | ||
Pulkit Goyal
|
r47576 | }) => { | ||
if use_detailed_exit_code { | ||||
*detailed_exit_code | ||||
} else { | ||||
Pulkit Goyal
|
r48199 | exit_codes::ABORT | ||
Pulkit Goyal
|
r47576 | } | ||
} | ||||
Pulkit Goyal
|
r48199 | Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL, | ||
Simon Sapin
|
r47174 | // Exit with a specific code and no error message to let a potential | ||
// wrapper script fallback to Python-based Mercurial. | ||||
Simon Sapin
|
r47424 | Err(CommandError::UnsupportedFeature { .. }) => { | ||
Pulkit Goyal
|
r48199 | exit_codes::UNIMPLEMENTED | ||
Simon Sapin
|
r47424 | } | ||
Raphaël Gomès
|
r50043 | Err(CommandError::InvalidFallback { .. }) => { | ||
exit_codes::INVALID_FALLBACK | ||||
} | ||||
Simon Sapin
|
r47343 | } | ||
Antoine Cezar
|
r45503 | } | ||
Antoine Cezar
|
r46100 | |||
Arseniy Alekseyev
|
r49961 | fn exit<'a>( | ||
original_args: &'a [OsString], | ||||
Simon Sapin
|
r47470 | initial_current_dir: &Option<PathBuf>, | ||
Simon Sapin
|
r47424 | ui: &Ui, | ||
Simon Sapin
|
r47425 | mut on_unsupported: OnUnsupported, | ||
Simon Sapin
|
r47424 | result: Result<(), CommandError>, | ||
Pulkit Goyal
|
r47576 | use_detailed_exit_code: bool, | ||
Simon Sapin
|
r47424 | ) -> ! { | ||
Simon Sapin
|
r47425 | if let ( | ||
OnUnsupported::Fallback { executable }, | ||||
Raphaël Gomès
|
r49622 | Err(CommandError::UnsupportedFeature { message }), | ||
Simon Sapin
|
r47425 | ) = (&on_unsupported, &result) | ||
{ | ||||
Arseniy Alekseyev
|
r49961 | let mut args = original_args.iter(); | ||
Arseniy Alekseyev
|
r49176 | let executable = match executable { | ||
None => { | ||||
exit_no_fallback( | ||||
ui, | ||||
OnUnsupported::Abort, | ||||
Err(CommandError::abort( | ||||
"abort: 'rhg.on-unsupported=fallback' without \ | ||||
'rhg.fallback-executable' set.", | ||||
)), | ||||
false, | ||||
); | ||||
} | ||||
Some(executable) => executable, | ||||
}; | ||||
Raphaël Gomès
|
r50809 | let executable_path = get_path_from_bytes(executable); | ||
Simon Sapin
|
r47425 | let this_executable = args.next().expect("exepcted argv[0] to exist"); | ||
Raphaël Gomès
|
r50826 | if executable_path == *this_executable { | ||
Simon Sapin
|
r47425 | // Avoid spawning infinitely many processes until resource | ||
// exhaustion. | ||||
let _ = ui.write_stderr(&format_bytes!( | ||||
b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ | ||||
points to `rhg` itself.\n", | ||||
executable | ||||
)); | ||||
on_unsupported = OnUnsupported::Abort | ||||
} else { | ||||
Raphaël Gomès
|
r49622 | log::debug!("falling back (see trace-level log)"); | ||
log::trace!("{}", local_to_utf8(message)); | ||||
Raphaël Gomès
|
r50043 | if let Err(err) = which::which(executable_path) { | ||
exit_no_fallback( | ||||
ui, | ||||
OnUnsupported::Abort, | ||||
Err(CommandError::InvalidFallback { | ||||
path: executable.to_owned(), | ||||
err: err.to_string(), | ||||
}), | ||||
use_detailed_exit_code, | ||||
) | ||||
} | ||||
Arseniy Alekseyev
|
r49176 | // `args` is now `argv[1..]` since we’ve already consumed | ||
// `argv[0]` | ||||
Simon Sapin
|
r47470 | let mut command = Command::new(executable_path); | ||
command.args(args); | ||||
if let Some(initial) = initial_current_dir { | ||||
command.current_dir(initial); | ||||
} | ||||
Raphaël Gomès
|
r50043 | // We don't use subprocess because proper signal handling is harder | ||
// and we don't want to keep `rhg` around after a fallback anyway. | ||||
// For example, if `rhg` is run in the background and falls back to | ||||
// `hg` which, in turn, waits for a signal, we'll get stuck if | ||||
// we're doing plain subprocess. | ||||
// | ||||
// If `exec` returns, we can only assume our process is very broken | ||||
// (see its documentation), so only try to forward the error code | ||||
// when exiting. | ||||
let err = command.exec(); | ||||
std::process::exit( | ||||
err.raw_os_error().unwrap_or(exit_codes::ABORT), | ||||
); | ||||
Simon Sapin
|
r47425 | } | ||
} | ||||
Pulkit Goyal
|
r47576 | exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code) | ||
Simon Sapin
|
r47482 | } | ||
fn exit_no_fallback( | ||||
ui: &Ui, | ||||
on_unsupported: OnUnsupported, | ||||
result: Result<(), CommandError>, | ||||
Pulkit Goyal
|
r47576 | use_detailed_exit_code: bool, | ||
Simon Sapin
|
r47482 | ) -> ! { | ||
Simon Sapin
|
r47424 | match &result { | ||
Ok(_) => {} | ||||
Simon Sapin
|
r47478 | Err(CommandError::Unsuccessful) => {} | ||
Raphaël Gomès
|
r50382 | Err(CommandError::Abort { message, hint, .. }) => { | ||
// Ignore errors when writing to stderr, we’re already exiting | ||||
// with failure code so there’s not much more we can do. | ||||
Simon Sapin
|
r47424 | if !message.is_empty() { | ||
Simon Sapin
|
r47465 | let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); | ||
Simon Sapin
|
r47424 | } | ||
Raphaël Gomès
|
r50382 | if let Some(hint) = hint { | ||
let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint)); | ||||
} | ||||
Simon Sapin
|
r47424 | } | ||
Err(CommandError::UnsupportedFeature { message }) => { | ||||
match on_unsupported { | ||||
OnUnsupported::Abort => { | ||||
let _ = ui.write_stderr(&format_bytes!( | ||||
b"unsupported feature: {}\n", | ||||
message | ||||
)); | ||||
} | ||||
OnUnsupported::AbortSilent => {} | ||||
Simon Sapin
|
r47425 | OnUnsupported::Fallback { .. } => unreachable!(), | ||
Simon Sapin
|
r47424 | } | ||
Simon Sapin
|
r47423 | } | ||
Raphaël Gomès
|
r50043 | Err(CommandError::InvalidFallback { path, err }) => { | ||
let _ = ui.write_stderr(&format_bytes!( | ||||
b"abort: invalid fallback '{}': {}\n", | ||||
path, | ||||
err.as_bytes(), | ||||
)); | ||||
} | ||||
Simon Sapin
|
r47423 | } | ||
Pulkit Goyal
|
r47576 | std::process::exit(exit_code(&result, use_detailed_exit_code)) | ||
Simon Sapin
|
r47423 | } | ||
Simon Sapin
|
r47252 | macro_rules! subcommands { | ||
($( $command: ident )+) => { | ||||
mod commands { | ||||
$( | ||||
pub mod $command; | ||||
)+ | ||||
} | ||||
Raphaël Gomès
|
r50534 | fn add_subcommand_args(app: clap::Command) -> clap::Command { | ||
Simon Sapin
|
r47252 | app | ||
$( | ||||
Simon Sapin
|
r47351 | .subcommand(commands::$command::args()) | ||
Simon Sapin
|
r47252 | )+ | ||
} | ||||
Simon Sapin
|
r47213 | |||
Simon Sapin
|
r47334 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; | ||
fn subcommand_run_fn(name: &str) -> Option<RunFn> { | ||||
Simon Sapin
|
r47252 | match name { | ||
$( | ||||
stringify!($command) => Some(commands::$command::run), | ||||
)+ | ||||
_ => None, | ||||
} | ||||
Antoine Cezar
|
r46113 | } | ||
Simon Sapin
|
r47252 | }; | ||
Antoine Cezar
|
r46100 | } | ||
Simon Sapin
|
r47252 | |||
subcommands! { | ||||
cat | ||||
debugdata | ||||
debugrequirements | ||||
Arseniy Alekseyev
|
r49178 | debugignorerhg | ||
Raphaël Gomès
|
r50379 | debugrhgsparse | ||
Simon Sapin
|
r47252 | files | ||
root | ||||
Simon Sapin
|
r47255 | config | ||
Georges Racinet
|
r47578 | status | ||
Simon Sapin
|
r47252 | } | ||
Georges Racinet
|
r47578 | |||
Simon Sapin
|
r47334 | pub struct CliInvocation<'a> { | ||
ui: &'a Ui, | ||||
Raphaël Gomès
|
r50534 | subcommand_args: &'a ArgMatches, | ||
Simon Sapin
|
r47423 | config: &'a Config, | ||
Simon Sapin
|
r47335 | /// References inside `Result` is a bit peculiar but allow | ||
/// `invocation.repo?` to work out with `&CliInvocation` since this | ||||
/// `Result` type is `Copy`. | ||||
repo: Result<&'a Repo, &'a NoRepoInCwdError>, | ||||
} | ||||
struct NoRepoInCwdError { | ||||
cwd: PathBuf, | ||||
Simon Sapin
|
r47334 | } | ||
Simon Sapin
|
r47335 | |||
Simon Sapin
|
r47423 | /// CLI arguments to be parsed "early" in order to be able to read | ||
/// configuration before using Clap. Ideally we would also use Clap for this, | ||||
/// see <https://github.com/clap-rs/clap/discussions/2366>. | ||||
/// | ||||
/// These arguments are still declared when we do use Clap later, so that Clap | ||||
/// does not return an error for their presence. | ||||
struct EarlyArgs { | ||||
/// Values of all `--config` arguments. (Possibly none) | ||||
config: Vec<Vec<u8>>, | ||||
Simon Sapin
|
r49583 | /// Value of all the `--color` argument, if any. | ||
color: Option<Vec<u8>>, | ||||
Simon Sapin
|
r47423 | /// Value of the `-R` or `--repository` argument, if any. | ||
repo: Option<Vec<u8>>, | ||||
Simon Sapin
|
r47470 | /// Value of the `--cwd` argument, if any. | ||
cwd: Option<Vec<u8>>, | ||||
Simon Sapin
|
r47423 | } | ||
impl EarlyArgs { | ||||
Arseniy Alekseyev
|
r49961 | fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self { | ||
Simon Sapin
|
r47423 | let mut args = args.into_iter().map(get_bytes_from_os_str); | ||
let mut config = Vec::new(); | ||||
Simon Sapin
|
r49583 | let mut color = None; | ||
Simon Sapin
|
r47423 | let mut repo = None; | ||
Simon Sapin
|
r47470 | let mut cwd = None; | ||
Simon Sapin
|
r47423 | // Use `while let` instead of `for` so that we can also call | ||
// `args.next()` inside the loop. | ||||
while let Some(arg) = args.next() { | ||||
if arg == b"--config" { | ||||
if let Some(value) = args.next() { | ||||
config.push(value) | ||||
} | ||||
} else if let Some(value) = arg.drop_prefix(b"--config=") { | ||||
config.push(value.to_owned()) | ||||
} | ||||
Simon Sapin
|
r49583 | if arg == b"--color" { | ||
if let Some(value) = args.next() { | ||||
color = Some(value) | ||||
} | ||||
} else if let Some(value) = arg.drop_prefix(b"--color=") { | ||||
color = Some(value.to_owned()) | ||||
} | ||||
Simon Sapin
|
r47470 | if arg == b"--cwd" { | ||
if let Some(value) = args.next() { | ||||
cwd = Some(value) | ||||
} | ||||
} else if let Some(value) = arg.drop_prefix(b"--cwd=") { | ||||
cwd = Some(value.to_owned()) | ||||
} | ||||
Simon Sapin
|
r47423 | if arg == b"--repository" || arg == b"-R" { | ||
if let Some(value) = args.next() { | ||||
repo = Some(value) | ||||
} | ||||
} else if let Some(value) = arg.drop_prefix(b"--repository=") { | ||||
repo = Some(value.to_owned()) | ||||
} else if let Some(value) = arg.drop_prefix(b"-R") { | ||||
repo = Some(value.to_owned()) | ||||
} | ||||
Simon Sapin
|
r47335 | } | ||
Simon Sapin
|
r49583 | Self { | ||
config, | ||||
color, | ||||
repo, | ||||
cwd, | ||||
} | ||||
Simon Sapin
|
r47335 | } | ||
} | ||||
Simon Sapin
|
r47424 | |||
/// What to do when encountering some unsupported feature. | ||||
/// | ||||
/// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`. | ||||
enum OnUnsupported { | ||||
/// Print an error message describing what feature is not supported, | ||||
/// and exit with code 252. | ||||
Abort, | ||||
/// Silently exit with code 252. | ||||
AbortSilent, | ||||
Simon Sapin
|
r47425 | /// Try running a Python implementation | ||
Arseniy Alekseyev
|
r49176 | Fallback { executable: Option<Vec<u8>> }, | ||
Simon Sapin
|
r47424 | } | ||
impl OnUnsupported { | ||||
Simon Sapin
|
r47425 | const DEFAULT: Self = OnUnsupported::Abort; | ||
Arseniy Alekseyev
|
r49176 | fn from_config(config: &Config) -> Self { | ||
Simon Sapin
|
r47425 | match config | ||
.get(b"rhg", b"on-unsupported") | ||||
.map(|value| value.to_ascii_lowercase()) | ||||
.as_deref() | ||||
{ | ||||
Simon Sapin
|
r47424 | Some(b"abort") => OnUnsupported::Abort, | ||
Some(b"abort-silent") => OnUnsupported::AbortSilent, | ||||
Simon Sapin
|
r47425 | Some(b"fallback") => OnUnsupported::Fallback { | ||
executable: config | ||||
.get(b"rhg", b"fallback-executable") | ||||
Arseniy Alekseyev
|
r49176 | .map(|x| x.to_owned()), | ||
Simon Sapin
|
r47425 | }, | ||
None => Self::DEFAULT, | ||||
Simon Sapin
|
r47424 | Some(_) => { | ||
// TODO: warn about unknown config value | ||||
Simon Sapin
|
r47425 | Self::DEFAULT | ||
Simon Sapin
|
r47424 | } | ||
} | ||||
} | ||||
} | ||||
Simon Sapin
|
r47467 | |||
Raphaël Gomès
|
r49270 | /// The `*` extension is an edge-case for config sub-options that apply to all | ||
/// extensions. For now, only `:required` exists, but that may change in the | ||||
/// future. | ||||
Raphaël Gomès
|
r50376 | const SUPPORTED_EXTENSIONS: &[&[u8]] = &[ | ||
b"blackbox", | ||||
b"share", | ||||
b"sparse", | ||||
b"narrow", | ||||
b"*", | ||||
b"strip", | ||||
b"rebase", | ||||
]; | ||||
Simon Sapin
|
r47467 | |||
fn check_extensions(config: &Config) -> Result<(), CommandError> { | ||||
Raphaël Gomès
|
r49829 | if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") { | ||
// All extensions are to be ignored, nothing to do here | ||||
return Ok(()); | ||||
} | ||||
Raphaël Gomès
|
r49270 | let enabled: HashSet<&[u8]> = config | ||
Raphaël Gomès
|
r50372 | .iter_section(b"extensions") | ||
.filter_map(|(extension, value)| { | ||||
if value == b"!" { | ||||
// Filter out disabled extensions | ||||
return None; | ||||
} | ||||
Raphaël Gomès
|
r49270 | // Ignore extension suboptions. Only `required` exists for now. | ||
// `rhg` either supports an extension or doesn't, so it doesn't | ||||
// make sense to consider the loading of an extension. | ||||
Raphaël Gomès
|
r50372 | let actual_extension = | ||
extension.split_2(b':').unwrap_or((extension, b"")).0; | ||||
Some(actual_extension) | ||||
Raphaël Gomès
|
r49270 | }) | ||
.collect(); | ||||
Simon Sapin
|
r47467 | |||
let mut unsupported = enabled; | ||||
for supported in SUPPORTED_EXTENSIONS { | ||||
unsupported.remove(supported); | ||||
} | ||||
Simon Sapin
|
r48763 | if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions") | ||
Simon Sapin
|
r47468 | { | ||
for ignored in ignored_list { | ||||
Simon Sapin
|
r48763 | unsupported.remove(ignored.as_slice()); | ||
Simon Sapin
|
r47468 | } | ||
} | ||||
Simon Sapin
|
r47467 | if unsupported.is_empty() { | ||
Ok(()) | ||||
} else { | ||||
Raphaël Gomès
|
r49842 | let mut unsupported: Vec<_> = unsupported.into_iter().collect(); | ||
// Sort the extensions to get a stable output | ||||
unsupported.sort(); | ||||
Simon Sapin
|
r47467 | Err(CommandError::UnsupportedFeature { | ||
message: format_bytes!( | ||||
Simon Sapin
|
r47468 | b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)", | ||
Simon Sapin
|
r47467 | join(unsupported, b", ") | ||
), | ||||
}) | ||||
} | ||||
} | ||||
Simon Sapin
|
r49159 | |||
r50087 | /// Array of tuples of (auto upgrade conf, feature conf, local requirement) | |||
Raphaël Gomès
|
r50826 | #[allow(clippy::type_complexity)] | ||
r50087 | const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[ | |||
( | ||||
("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"), | ||||
("format", "use-share-safe"), | ||||
requirements::SHARESAFE_REQUIREMENT, | ||||
), | ||||
r50089 | ( | |||
("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"), | ||||
("format", "use-dirstate-tracked-hint"), | ||||
requirements::DIRSTATE_TRACKED_HINT_V1, | ||||
), | ||||
r50090 | ( | |||
Arseniy Alekseyev
|
r50395 | ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"), | ||
r50090 | ("format", "use-dirstate-v2"), | |||
requirements::DIRSTATE_V2_REQUIREMENT, | ||||
), | ||||
r50087 | ]; | |||
/// Mercurial allows users to automatically upgrade their repository. | ||||
/// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade | ||||
/// is needed. | ||||
fn check_auto_upgrade( | ||||
config: &Config, | ||||
reqs: &HashSet<String>, | ||||
) -> Result<(), CommandError> { | ||||
for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() { | ||||
let auto_upgrade = config | ||||
.get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?; | ||||
if auto_upgrade { | ||||
let want_it = config.get_bool( | ||||
feature_conf.0.as_bytes(), | ||||
feature_conf.1.as_bytes(), | ||||
)?; | ||||
let have_it = reqs.contains(*local_req); | ||||
let action = match (want_it, have_it) { | ||||
(true, false) => Some("upgrade"), | ||||
(false, true) => Some("downgrade"), | ||||
_ => None, | ||||
}; | ||||
if let Some(action) = action { | ||||
let message = format!( | ||||
"automatic {} {}.{}", | ||||
action, upgrade_conf.0, upgrade_conf.1 | ||||
); | ||||
return Err(CommandError::unsupported(message)); | ||||
} | ||||
} | ||||
} | ||||
Ok(()) | ||||
} | ||||
Simon Sapin
|
r49163 | fn check_unsupported( | ||
config: &Config, | ||||
Simon Sapin
|
r49341 | repo: Result<&Repo, &NoRepoInCwdError>, | ||
Simon Sapin
|
r49163 | ) -> Result<(), CommandError> { | ||
Simon Sapin
|
r49159 | check_extensions(config)?; | ||
if std::env::var_os("HG_PENDING").is_some() { | ||||
// TODO: only if the value is `== repo.working_directory`? | ||||
// What about relative v.s. absolute paths? | ||||
Err(CommandError::unsupported("$HG_PENDING"))? | ||||
} | ||||
Simon Sapin
|
r49341 | if let Ok(repo) = repo { | ||
if repo.has_subrepos()? { | ||||
Err(CommandError::unsupported("sub-repositories"))? | ||||
} | ||||
r50087 | check_auto_upgrade(config, repo.requirements())?; | |||
Simon Sapin
|
r49341 | } | ||
Simon Sapin
|
r49162 | if config.has_non_empty_section(b"encode") { | ||
Err(CommandError::unsupported("[encode] config"))? | ||||
} | ||||
if config.has_non_empty_section(b"decode") { | ||||
Err(CommandError::unsupported("[decode] config"))? | ||||
} | ||||
Simon Sapin
|
r49159 | Ok(()) | ||
} | ||||