This patch adds basic support for detailed exit code to rhg with support for
ConfigParseError.
For now, if parsing the config results in error, we silently fallbacks to
false. The python version in this case emits a traceback.
SimonSapin |
hg-reviewers |
This patch adds basic support for detailed exit code to rhg with support for
ConfigParseError.
For now, if parsing the config results in error, we silently fallbacks to
false. The python version in this case emits a traceback.
Automatic diff as part of commit; lint not applicable. |
Automatic diff as part of commit; unit tests not applicable. |
Looks good, thanks!
rust/rhg/src/error.rs | ||
---|---|---|
47 | No need to change this unless you want to, but this line can be just detailed_exit_code, (This is syntactic sugar in a struct literal when a field is initialized with a local variable of the same name.) |
Path | Packages | |||
---|---|---|---|---|
M | rust/rhg/src/error.rs (22 lines) | |||
M | rust/rhg/src/exitcode.rs (3 lines) | |||
M | rust/rhg/src/main.rs (68 lines) | |||
M | tests/test-config.t (3 lines) | |||
M | tests/test-dispatch.t (3 lines) |
pub type ExitCode = i32; | pub type ExitCode = i32; | ||||
/// Successful exit | /// Successful exit | ||||
pub const OK: ExitCode = 0; | pub const OK: ExitCode = 0; | ||||
/// Generic abort | /// Generic abort | ||||
pub const ABORT: ExitCode = 255; | pub const ABORT: ExitCode = 255; | ||||
// Abort when there is a config related error | |||||
pub const CONFIG_ERROR_ABORT: ExitCode = 30; | |||||
/// Generic something completed but did not succeed | /// Generic something completed but did not succeed | ||||
pub const UNSUCCESSFUL: ExitCode = 1; | pub const UNSUCCESSFUL: ExitCode = 1; | ||||
/// Command or feature not implemented by rhg | /// Command or feature not implemented by rhg | ||||
pub const UNIMPLEMENTED: ExitCode = 252; | pub const UNIMPLEMENTED: ExitCode = 252; |
ui, | ui, | ||||
subcommand_args, | subcommand_args, | ||||
config, | config, | ||||
repo, | repo, | ||||
}; | }; | ||||
let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; | ||||
blackbox.log_command_start(); | blackbox.log_command_start(); | ||||
let result = run(&invocation); | let result = run(&invocation); | ||||
blackbox.log_command_end(exit_code(&result)); | blackbox.log_command_end(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), | |||||
)); | |||||
result | result | ||||
} | } | ||||
fn main() { | fn main() { | ||||
// Run this first, before we find out if the blackbox extension is even | // Run this first, before we find out if the blackbox extension is even | ||||
// enabled, in order to include everything in-between in the duration | // enabled, in order to include everything in-between in the duration | ||||
// measurements. Reading config files can be slow if they’re on NFS. | // measurements. Reading config files can be slow if they’re on NFS. | ||||
let process_start_time = blackbox::ProcessStartTime::now(); | let process_start_time = blackbox::ProcessStartTime::now(); | ||||
&None, | &None, | ||||
&ui, | &ui, | ||||
OnUnsupported::Abort, | OnUnsupported::Abort, | ||||
Err(CommandError::abort(format!( | Err(CommandError::abort(format!( | ||||
"abort: {}: '{}'", | "abort: {}: '{}'", | ||||
error, | error, | ||||
cwd.display() | cwd.display() | ||||
))), | ))), | ||||
false, | |||||
) | ) | ||||
}) | }) | ||||
}); | }); | ||||
let non_repo_config = | let non_repo_config = | ||||
Config::load(early_args.config).unwrap_or_else(|error| { | Config::load(early_args.config).unwrap_or_else(|error| { | ||||
// Normally this is decided based on config, but we don’t have that | // Normally this is decided based on config, but we don’t have that | ||||
// available. As of this writing config loading never returns an | // available. As of this writing config loading never returns an | ||||
// "unsupported" error but that is not enforced by the type system. | // "unsupported" error but that is not enforced by the type system. | ||||
let on_unsupported = OnUnsupported::Abort; | let on_unsupported = OnUnsupported::Abort; | ||||
exit(&initial_current_dir, &ui, on_unsupported, Err(error.into())) | exit( | ||||
&initial_current_dir, | |||||
&ui, | |||||
on_unsupported, | |||||
Err(error.into()), | |||||
false, | |||||
) | |||||
}); | }); | ||||
if let Some(repo_path_bytes) = &early_args.repo { | if let Some(repo_path_bytes) = &early_args.repo { | ||||
lazy_static::lazy_static! { | lazy_static::lazy_static! { | ||||
static ref SCHEME_RE: regex::bytes::Regex = | static ref SCHEME_RE: regex::bytes::Regex = | ||||
// Same as `_matchscheme` in `mercurial/util.py` | // Same as `_matchscheme` in `mercurial/util.py` | ||||
regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); | regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); | ||||
} | } | ||||
if SCHEME_RE.is_match(&repo_path_bytes) { | if SCHEME_RE.is_match(&repo_path_bytes) { | ||||
exit( | exit( | ||||
&initial_current_dir, | &initial_current_dir, | ||||
&ui, | &ui, | ||||
OnUnsupported::from_config(&ui, &non_repo_config), | OnUnsupported::from_config(&ui, &non_repo_config), | ||||
Err(CommandError::UnsupportedFeature { | Err(CommandError::UnsupportedFeature { | ||||
message: format_bytes!( | message: format_bytes!( | ||||
b"URL-like --repository {}", | b"URL-like --repository {}", | ||||
repo_path_bytes | repo_path_bytes | ||||
), | ), | ||||
}), | }), | ||||
// 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 repo_path = early_args.repo.as_deref().map(get_path_from_bytes); | let repo_path = early_args.repo.as_deref().map(get_path_from_bytes); | ||||
let repo_result = match Repo::find(&non_repo_config, repo_path) { | let repo_result = match Repo::find(&non_repo_config, repo_path) { | ||||
Ok(repo) => Ok(repo), | Ok(repo) => Ok(repo), | ||||
Err(RepoError::NotFound { at }) if repo_path.is_none() => { | Err(RepoError::NotFound { at }) if repo_path.is_none() => { | ||||
// Not finding a repo is not fatal yet, if `-R` was not given | // Not finding a repo is not fatal yet, if `-R` was not given | ||||
Err(NoRepoInCwdError { cwd: at }) | Err(NoRepoInCwdError { cwd: at }) | ||||
} | } | ||||
Err(error) => exit( | Err(error) => exit( | ||||
&initial_current_dir, | &initial_current_dir, | ||||
&ui, | &ui, | ||||
OnUnsupported::from_config(&ui, &non_repo_config), | OnUnsupported::from_config(&ui, &non_repo_config), | ||||
Err(error.into()), | Err(error.into()), | ||||
// 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 config = if let Ok(repo) = &repo_result { | let config = if let Ok(repo) = &repo_result { | ||||
repo.config() | repo.config() | ||||
} else { | } else { | ||||
&non_repo_config | &non_repo_config | ||||
}; | }; | ||||
let on_unsupported = OnUnsupported::from_config(&ui, config); | let on_unsupported = OnUnsupported::from_config(&ui, config); | ||||
let result = main_with_result( | let result = main_with_result( | ||||
&process_start_time, | &process_start_time, | ||||
&ui, | &ui, | ||||
repo_result.as_ref(), | repo_result.as_ref(), | ||||
config, | config, | ||||
); | ); | ||||
exit(&initial_current_dir, &ui, on_unsupported, result) | exit( | ||||
&initial_current_dir, | |||||
&ui, | |||||
on_unsupported, | |||||
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), | |||||
) | |||||
} | } | ||||
fn exit_code(result: &Result<(), CommandError>) -> i32 { | fn exit_code( | ||||
result: &Result<(), CommandError>, | |||||
use_detailed_exit_code: bool, | |||||
) -> i32 { | |||||
match result { | match result { | ||||
Ok(()) => exitcode::OK, | Ok(()) => exitcode::OK, | ||||
Err(CommandError::Abort { .. }) => exitcode::ABORT, | Err(CommandError::Abort { | ||||
message: _, | |||||
detailed_exit_code, | |||||
}) => { | |||||
if use_detailed_exit_code { | |||||
*detailed_exit_code | |||||
} else { | |||||
exitcode::ABORT | |||||
} | |||||
} | |||||
Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL, | Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL, | ||||
// Exit with a specific code and no error message to let a potential | // Exit with a specific code and no error message to let a potential | ||||
// wrapper script fallback to Python-based Mercurial. | // wrapper script fallback to Python-based Mercurial. | ||||
Err(CommandError::UnsupportedFeature { .. }) => { | Err(CommandError::UnsupportedFeature { .. }) => { | ||||
exitcode::UNIMPLEMENTED | exitcode::UNIMPLEMENTED | ||||
} | } | ||||
} | } | ||||
} | } | ||||
fn exit( | fn exit( | ||||
initial_current_dir: &Option<PathBuf>, | initial_current_dir: &Option<PathBuf>, | ||||
ui: &Ui, | ui: &Ui, | ||||
mut on_unsupported: OnUnsupported, | mut on_unsupported: OnUnsupported, | ||||
result: Result<(), CommandError>, | result: Result<(), CommandError>, | ||||
use_detailed_exit_code: bool, | |||||
) -> ! { | ) -> ! { | ||||
if let ( | if let ( | ||||
OnUnsupported::Fallback { executable }, | OnUnsupported::Fallback { executable }, | ||||
Err(CommandError::UnsupportedFeature { .. }), | Err(CommandError::UnsupportedFeature { .. }), | ||||
) = (&on_unsupported, &result) | ) = (&on_unsupported, &result) | ||||
{ | { | ||||
let mut args = std::env::args_os(); | let mut args = std::env::args_os(); | ||||
let executable_path = get_path_from_bytes(&executable); | let executable_path = get_path_from_bytes(&executable); | ||||
b"tried to fall back to a '{}' sub-process but got error {}\n", | b"tried to fall back to a '{}' sub-process but got error {}\n", | ||||
executable, format_bytes::Utf8(error) | executable, format_bytes::Utf8(error) | ||||
)); | )); | ||||
on_unsupported = OnUnsupported::Abort | on_unsupported = OnUnsupported::Abort | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
exit_no_fallback(ui, on_unsupported, result) | exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code) | ||||
} | } | ||||
fn exit_no_fallback( | fn exit_no_fallback( | ||||
ui: &Ui, | ui: &Ui, | ||||
on_unsupported: OnUnsupported, | on_unsupported: OnUnsupported, | ||||
result: Result<(), CommandError>, | result: Result<(), CommandError>, | ||||
use_detailed_exit_code: bool, | |||||
) -> ! { | ) -> ! { | ||||
match &result { | match &result { | ||||
Ok(_) => {} | Ok(_) => {} | ||||
Err(CommandError::Unsuccessful) => {} | Err(CommandError::Unsuccessful) => {} | ||||
Err(CommandError::Abort { message }) => { | Err(CommandError::Abort { | ||||
message, | |||||
detailed_exit_code: _, | |||||
}) => { | |||||
if !message.is_empty() { | if !message.is_empty() { | ||||
// Ignore errors when writing to stderr, we’re already exiting | // Ignore errors when writing to stderr, we’re already exiting | ||||
// with failure code so there’s not much more we can do. | // with failure code so there’s not much more we can do. | ||||
let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); | let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); | ||||
} | } | ||||
} | } | ||||
Err(CommandError::UnsupportedFeature { message }) => { | Err(CommandError::UnsupportedFeature { message }) => { | ||||
match on_unsupported { | match on_unsupported { | ||||
OnUnsupported::Abort => { | OnUnsupported::Abort => { | ||||
let _ = ui.write_stderr(&format_bytes!( | let _ = ui.write_stderr(&format_bytes!( | ||||
b"unsupported feature: {}\n", | b"unsupported feature: {}\n", | ||||
message | message | ||||
)); | )); | ||||
} | } | ||||
OnUnsupported::AbortSilent => {} | OnUnsupported::AbortSilent => {} | ||||
OnUnsupported::Fallback { .. } => unreachable!(), | OnUnsupported::Fallback { .. } => unreachable!(), | ||||
} | } | ||||
} | } | ||||
} | } | ||||
std::process::exit(exit_code(&result)) | std::process::exit(exit_code(&result, use_detailed_exit_code)) | ||||
} | } | ||||
macro_rules! subcommands { | macro_rules! subcommands { | ||||
($( $command: ident )+) => { | ($( $command: ident )+) => { | ||||
mod commands { | mod commands { | ||||
$( | $( | ||||
pub mod $command; | pub mod $command; | ||||
)+ | )+ | ||||
.unwrap_or_else(|| { | .unwrap_or_else(|| { | ||||
exit_no_fallback( | exit_no_fallback( | ||||
ui, | ui, | ||||
Self::Abort, | Self::Abort, | ||||
Err(CommandError::abort( | Err(CommandError::abort( | ||||
"abort: 'rhg.on-unsupported=fallback' without \ | "abort: 'rhg.on-unsupported=fallback' without \ | ||||
'rhg.fallback-executable' set." | 'rhg.fallback-executable' set." | ||||
)), | )), | ||||
false, | |||||
) | ) | ||||
}) | }) | ||||
.to_owned(), | .to_owned(), | ||||
}, | }, | ||||
None => Self::DEFAULT, | None => Self::DEFAULT, | ||||
Some(_) => { | Some(_) => { | ||||
// TODO: warn about unknown config value | // TODO: warn about unknown config value | ||||
Self::DEFAULT | Self::DEFAULT |
hide outer repo | hide outer repo | ||||
$ hg init | $ hg init | ||||
Invalid syntax: no value | Invalid syntax: no value | ||||
TODO: add rhg support for detailed exit codes | |||||
#if no-rhg | |||||
$ cat > .hg/hgrc << EOF | $ cat > .hg/hgrc << EOF | ||||
> novaluekey | > novaluekey | ||||
> EOF | > EOF | ||||
$ hg showconfig | $ hg showconfig | ||||
config error at $TESTTMP/.hg/hgrc:1: novaluekey | config error at $TESTTMP/.hg/hgrc:1: novaluekey | ||||
[30] | [30] | ||||
Invalid syntax: no key | Invalid syntax: no key | ||||
$ cat > .hg/hgrc << EOF | $ cat > .hg/hgrc << EOF | ||||
> [section] | > [section] | ||||
> key=value | > key=value | ||||
> EOF | > EOF | ||||
$ hg showconfig | $ hg showconfig | ||||
config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: [section] | config error at $TESTTMP/.hg/hgrc:1: unexpected leading whitespace: [section] | ||||
[30] | [30] | ||||
#endif | |||||
Reset hgrc | Reset hgrc | ||||
$ echo > .hg/hgrc | $ echo > .hg/hgrc | ||||
Test case sensitive configuration | Test case sensitive configuration | ||||
$ cat <<EOF >> $HGRCPATH | $ cat <<EOF >> $HGRCPATH |
> EOF | > EOF | ||||
$ hg log -b '--config=extensions.bad=bad.py' default | $ hg log -b '--config=extensions.bad=bad.py' default | ||||
*** failed to import extension bad from bad.py: bad | *** failed to import extension bad from bad.py: bad | ||||
abort: option --config may not be abbreviated | abort: option --config may not be abbreviated | ||||
[10] | [10] | ||||
$ mkdir -p badrepo/.hg | $ mkdir -p badrepo/.hg | ||||
$ echo 'invalid-syntax' > badrepo/.hg/hgrc | $ echo 'invalid-syntax' > badrepo/.hg/hgrc | ||||
TODO: add rhg support for detailed exit codes | |||||
#if no-rhg | |||||
$ hg log -b -Rbadrepo default | $ hg log -b -Rbadrepo default | ||||
config error at badrepo/.hg/hgrc:1: invalid-syntax | config error at badrepo/.hg/hgrc:1: invalid-syntax | ||||
[30] | [30] | ||||
#endif | |||||
$ hg log -b --cwd=inexistent default | $ hg log -b --cwd=inexistent default | ||||
abort: $ENOENT$: 'inexistent' | abort: $ENOENT$: 'inexistent' | ||||
[255] | [255] | ||||
$ hg log -b '--config=ui.traceback=yes' 2>&1 | grep '^Traceback' | $ hg log -b '--config=ui.traceback=yes' 2>&1 | grep '^Traceback' | ||||
Traceback (most recent call last): | Traceback (most recent call last): | ||||
$ hg log -b '--config=profiling.enabled=yes' 2>&1 | grep -i sample | $ hg log -b '--config=profiling.enabled=yes' 2>&1 | grep -i sample |
No need to change this unless you want to, but this line can be just detailed_exit_code,
(This is syntactic sugar in a struct literal when a field is initialized with a local variable of the same name.)