Details
Details
- Reviewers
Alphare - Group Reviewers
hg-reviewers - Commits
- rHGca3f73cc3cf4: rhg: Simplify CommandError based on its use
Diff Detail
Diff Detail
- Repository
- rHG Mercurial
- Branch
- default
- Lint
No Linters Available - Unit
No Unit Test Coverage
Alphare |
hg-reviewers |
No Linters Available |
No Unit Test Coverage |
Path | Packages | |||
---|---|---|---|---|
M | rust/hg-core/src/utils.rs (8 lines) | |||
M | rust/rhg/src/commands/cat.rs (9 lines) | |||
M | rust/rhg/src/commands/files.rs (3 lines) | |||
M | rust/rhg/src/error.rs (124 lines) | |||
M | rust/rhg/src/exitcode.rs (4 lines) | |||
M | rust/rhg/src/main.rs (32 lines) | |||
M | tests/test-rhg.t (4 lines) |
Commit | Parents | Author | Summary | Date |
---|---|---|---|---|
5d2d86f3ce4a | eebc8849f794 | Simon Sapin | Jan 28 2021, 1:13 PM |
Status | Author | Revision | |
---|---|---|---|
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin | ||
Closed | SimonSapin |
// utils module | // utils module | ||||
// | // | ||||
// Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | // Copyright 2019 Raphaël Gomès <rgomes@octobus.net> | ||||
// | // | ||||
// This software may be used and distributed according to the terms of the | // This software may be used and distributed according to the terms of the | ||||
// GNU General Public License version 2 or any later version. | // GNU General Public License version 2 or any later version. | ||||
//! Contains useful functions, traits, structs, etc. for use in core. | //! Contains useful functions, traits, structs, etc. for use in core. | ||||
use crate::errors::{HgError, IoErrorContext}; | |||||
use crate::utils::hg_path::HgPath; | use crate::utils::hg_path::HgPath; | ||||
use std::{io::Write, ops::Deref}; | use std::{io::Write, ops::Deref}; | ||||
pub mod files; | pub mod files; | ||||
pub mod hg_path; | pub mod hg_path; | ||||
pub mod path_auditor; | pub mod path_auditor; | ||||
/// Useful until rust/issues/56345 is stable | /// Useful until rust/issues/56345 is stable | ||||
// TODO: use the str method when we require Rust 1.45 | // TODO: use the str method when we require Rust 1.45 | ||||
pub(crate) fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> { | pub(crate) fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> { | ||||
if s.ends_with(suffix) { | if s.ends_with(suffix) { | ||||
Some(&s[..s.len() - suffix.len()]) | Some(&s[..s.len() - suffix.len()]) | ||||
} else { | } else { | ||||
None | None | ||||
} | } | ||||
} | } | ||||
pub fn current_dir() -> Result<std::path::PathBuf, HgError> { | |||||
std::env::current_dir().map_err(|error| HgError::IoError { | |||||
error, | |||||
context: IoErrorContext::CurrentDir, | |||||
}) | |||||
} |
} | } | ||||
} | } | ||||
impl<'a> Command for CatCommand<'a> { | impl<'a> Command for CatCommand<'a> { | ||||
#[timed] | #[timed] | ||||
fn run(&self, ui: &Ui) -> Result<(), CommandError> { | fn run(&self, ui: &Ui) -> Result<(), CommandError> { | ||||
let repo = Repo::find()?; | let repo = Repo::find()?; | ||||
repo.check_requirements()?; | repo.check_requirements()?; | ||||
let cwd = std::env::current_dir() | let cwd = hg::utils::current_dir()?; | ||||
.or_else(|e| Err(CommandError::CurrentDirNotFound(e)))?; | |||||
let mut files = vec![]; | let mut files = vec![]; | ||||
for file in self.files.iter() { | for file in self.files.iter() { | ||||
// TODO: actually normalize `..` path segments etc? | |||||
let normalized = cwd.join(&file); | let normalized = cwd.join(&file); | ||||
let stripped = normalized | let stripped = normalized | ||||
.strip_prefix(&repo.working_directory_path()) | .strip_prefix(&repo.working_directory_path()) | ||||
.or(Err(CommandError::Abort(None)))?; | // TODO: error message for path arguments outside of the repo | ||||
.map_err(|_| CommandError::abort(""))?; | |||||
let hg_file = HgPathBuf::try_from(stripped.to_path_buf()) | let hg_file = HgPathBuf::try_from(stripped.to_path_buf()) | ||||
.or(Err(CommandError::Abort(None)))?; | .map_err(|e| CommandError::abort(e.to_string()))?; | ||||
files.push(hg_file); | files.push(hg_file); | ||||
} | } | ||||
match self.rev { | match self.rev { | ||||
Some(rev) => { | Some(rev) => { | ||||
let data = cat(&repo, rev, &files).map_err(|e| (e, rev))?; | let data = cat(&repo, rev, &files).map_err(|e| (e, rev))?; | ||||
self.display(ui, &data) | self.display(ui, &data) | ||||
} | } | ||||
None => Err(CommandError::Unimplemented.into()), | None => Err(CommandError::Unimplemented.into()), | ||||
} | } | ||||
} | } | ||||
} | } |
} | } | ||||
fn display_files( | fn display_files( | ||||
&self, | &self, | ||||
ui: &Ui, | ui: &Ui, | ||||
repo: &Repo, | repo: &Repo, | ||||
files: impl IntoIterator<Item = &'a HgPath>, | files: impl IntoIterator<Item = &'a HgPath>, | ||||
) -> Result<(), CommandError> { | ) -> Result<(), CommandError> { | ||||
let cwd = std::env::current_dir() | let cwd = hg::utils::current_dir()?; | ||||
.or_else(|e| Err(CommandError::CurrentDirNotFound(e)))?; | |||||
let rooted_cwd = cwd | let rooted_cwd = cwd | ||||
.strip_prefix(repo.working_directory_path()) | .strip_prefix(repo.working_directory_path()) | ||||
.expect("cwd was already checked within the repository"); | .expect("cwd was already checked within the repository"); | ||||
let rooted_cwd = HgPathBuf::from(get_bytes_from_path(rooted_cwd)); | let rooted_cwd = HgPathBuf::from(get_bytes_from_path(rooted_cwd)); | ||||
let mut stdout = ui.stdout_buffer(); | let mut stdout = ui.stdout_buffer(); | ||||
for file in files { | for file in files { |
use crate::exitcode; | |||||
use crate::ui::utf8_to_local; | use crate::ui::utf8_to_local; | ||||
use crate::ui::UiError; | use crate::ui::UiError; | ||||
use format_bytes::format_bytes; | use hg::errors::{HgError, IoErrorContext}; | ||||
use hg::errors::HgError; | |||||
use hg::operations::FindRootError; | use hg::operations::FindRootError; | ||||
use hg::revlog::revlog::RevlogError; | use hg::revlog::revlog::RevlogError; | ||||
use hg::utils::files::get_bytes_from_path; | |||||
use std::convert::From; | use std::convert::From; | ||||
use std::path::PathBuf; | |||||
/// The kind of command error | /// The kind of command error | ||||
#[derive(Debug, derive_more::From)] | #[derive(Debug)] | ||||
pub enum CommandError { | pub enum CommandError { | ||||
/// The root of the repository cannot be found | /// Exit with an error message and "standard" failure exit code. | ||||
RootNotFound(PathBuf), | Abort { message: Vec<u8> }, | ||||
/// The current directory cannot be found | |||||
CurrentDirNotFound(std::io::Error), | |||||
/// The standard output stream cannot be written to | |||||
StdoutError, | |||||
/// The standard error stream cannot be written to | |||||
StderrError, | |||||
/// The command aborted | |||||
Abort(Option<Vec<u8>>), | |||||
/// A mercurial capability as not been implemented. | /// A mercurial capability as not been implemented. | ||||
/// | |||||
/// There is no error message printed in this case. | |||||
/// Instead, we exit with a specic status code and a wrapper script may | |||||
/// fallback to Python-based Mercurial. | |||||
Unimplemented, | Unimplemented, | ||||
/// Common cases | |||||
#[from] | |||||
Other(HgError), | |||||
} | } | ||||
impl CommandError { | impl CommandError { | ||||
pub fn get_exit_code(&self) -> exitcode::ExitCode { | pub fn abort(message: impl AsRef<str>) -> Self { | ||||
match self { | CommandError::Abort { | ||||
CommandError::RootNotFound(_) => exitcode::ABORT, | // TODO: bytes-based (instead of Unicode-based) formatting | ||||
CommandError::CurrentDirNotFound(_) => exitcode::ABORT, | // of error messages to handle non-UTF-8 filenames etc: | ||||
CommandError::StdoutError => exitcode::ABORT, | // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output | ||||
CommandError::StderrError => exitcode::ABORT, | message: utf8_to_local(message.as_ref()).into(), | ||||
CommandError::Abort(_) => exitcode::ABORT, | |||||
CommandError::Unimplemented => exitcode::UNIMPLEMENTED_COMMAND, | |||||
CommandError::Other(HgError::UnsupportedFeature(_)) => { | |||||
exitcode::UNIMPLEMENTED_COMMAND | |||||
} | |||||
CommandError::Other(_) => exitcode::ABORT, | |||||
} | |||||
} | |||||
/// Return the message corresponding to the error if any | |||||
pub fn get_error_message_bytes(&self) -> Option<Vec<u8>> { | |||||
match self { | |||||
CommandError::RootNotFound(path) => { | |||||
let bytes = get_bytes_from_path(path); | |||||
Some(format_bytes!( | |||||
b"abort: no repository found in '{}' (.hg not found)!\n", | |||||
bytes.as_slice() | |||||
)) | |||||
} | |||||
CommandError::CurrentDirNotFound(e) => Some(format_bytes!( | |||||
b"abort: error getting current working directory: {}\n", | |||||
e.to_string().as_bytes(), | |||||
)), | |||||
CommandError::Abort(message) => message.to_owned(), | |||||
CommandError::StdoutError | |||||
| CommandError::StderrError | |||||
| CommandError::Unimplemented | |||||
| CommandError::Other(HgError::UnsupportedFeature(_)) => None, | |||||
CommandError::Other(e) => { | |||||
Some(format_bytes!(b"{}\n", e.to_string().as_bytes())) | |||||
} | } | ||||
} | } | ||||
} | } | ||||
/// Exist the process with the corresponding exit code. | impl From<HgError> for CommandError { | ||||
pub fn exit(&self) { | fn from(error: HgError) -> Self { | ||||
std::process::exit(self.get_exit_code()) | match error { | ||||
HgError::UnsupportedFeature(_) => CommandError::Unimplemented, | |||||
_ => CommandError::abort(error.to_string()), | |||||
} | |||||
} | } | ||||
} | } | ||||
impl From<UiError> for CommandError { | impl From<UiError> for CommandError { | ||||
fn from(error: UiError) -> Self { | fn from(_error: UiError) -> Self { | ||||
match error { | // If we already failed writing to stdout or stderr, | ||||
UiError::StdoutError(_) => CommandError::StdoutError, | // writing an error message to stderr about it would be likely to fail | ||||
UiError::StderrError(_) => CommandError::StderrError, | // too. | ||||
} | CommandError::abort("") | ||||
} | } | ||||
} | } | ||||
impl From<FindRootError> for CommandError { | impl From<FindRootError> for CommandError { | ||||
fn from(err: FindRootError) -> Self { | fn from(err: FindRootError) -> Self { | ||||
match err { | match err { | ||||
FindRootError::RootNotFound(path) => { | FindRootError::RootNotFound(path) => CommandError::abort(format!( | ||||
CommandError::RootNotFound(path) | "no repository found in '{}' (.hg not found)!", | ||||
} | path.display() | ||||
FindRootError::GetCurrentDirError(e) => { | )), | ||||
CommandError::CurrentDirNotFound(e) | FindRootError::GetCurrentDirError(error) => HgError::IoError { | ||||
error, | |||||
context: IoErrorContext::CurrentDir, | |||||
} | } | ||||
.into(), | |||||
} | } | ||||
} | } | ||||
} | } | ||||
impl From<(RevlogError, &str)> for CommandError { | impl From<(RevlogError, &str)> for CommandError { | ||||
fn from((err, rev): (RevlogError, &str)) -> CommandError { | fn from((err, rev): (RevlogError, &str)) -> CommandError { | ||||
match err { | match err { | ||||
RevlogError::InvalidRevision => CommandError::Abort(Some( | RevlogError::InvalidRevision => CommandError::abort(format!( | ||||
utf8_to_local(&format!( | "invalid revision identifier {}", | ||||
"abort: invalid revision identifier {}\n", | |||||
rev | rev | ||||
)) | |||||
.into(), | |||||
)), | )), | ||||
RevlogError::AmbiguousPrefix => CommandError::Abort(Some( | RevlogError::AmbiguousPrefix => CommandError::abort(format!( | ||||
utf8_to_local(&format!( | "ambiguous revision identifier {}", | ||||
"abort: ambiguous revision identifier {}\n", | |||||
rev | rev | ||||
)) | |||||
.into(), | |||||
)), | )), | ||||
RevlogError::Other(err) => CommandError::Other(err), | RevlogError::Other(error) => error.into(), | ||||
} | } | ||||
} | } | ||||
} | } |
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; | ||||
/// Command not implemented by rhg | /// Command or feature not implemented by rhg | ||||
pub const UNIMPLEMENTED_COMMAND: ExitCode = 252; | pub const UNIMPLEMENTED: ExitCode = 252; |
extern crate log; | extern crate log; | ||||
use clap::App; | use clap::App; | ||||
use clap::AppSettings; | use clap::AppSettings; | ||||
use clap::Arg; | use clap::Arg; | ||||
use clap::ArgGroup; | use clap::ArgGroup; | ||||
use clap::ArgMatches; | use clap::ArgMatches; | ||||
use clap::SubCommand; | use clap::SubCommand; | ||||
use format_bytes::format_bytes; | |||||
use hg::operations::DebugDataKind; | use hg::operations::DebugDataKind; | ||||
use std::convert::TryFrom; | use std::convert::TryFrom; | ||||
mod commands; | mod commands; | ||||
mod error; | mod error; | ||||
mod exitcode; | mod exitcode; | ||||
mod ui; | mod ui; | ||||
use commands::Command; | use commands::Command; | ||||
) | ) | ||||
.subcommand( | .subcommand( | ||||
SubCommand::with_name("debugrequirements") | SubCommand::with_name("debugrequirements") | ||||
.about(commands::debugrequirements::HELP_TEXT), | .about(commands::debugrequirements::HELP_TEXT), | ||||
); | ); | ||||
let matches = app.clone().get_matches_safe().unwrap_or_else(|err| { | let matches = app.clone().get_matches_safe().unwrap_or_else(|err| { | ||||
let _ = ui::Ui::new().writeln_stderr_str(&err.message); | let _ = ui::Ui::new().writeln_stderr_str(&err.message); | ||||
std::process::exit(exitcode::UNIMPLEMENTED_COMMAND) | std::process::exit(exitcode::UNIMPLEMENTED) | ||||
}); | }); | ||||
let ui = ui::Ui::new(); | let ui = ui::Ui::new(); | ||||
let command_result = match_subcommand(matches, &ui); | let command_result = match_subcommand(matches, &ui); | ||||
match command_result { | let exit_code = match command_result { | ||||
Ok(_) => std::process::exit(exitcode::OK), | Ok(_) => exitcode::OK, | ||||
Err(e) => { | |||||
let message = e.get_error_message_bytes(); | // Exit with a specific code and no error message to let a potential | ||||
if let Some(msg) = message { | // wrapper script fallback to Python-based Mercurial. | ||||
match ui.write_stderr(&msg) { | Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED, | ||||
Ok(_) => (), | |||||
Err(_) => std::process::exit(exitcode::ABORT), | Err(CommandError::Abort { message }) => { | ||||
}; | if !message.is_empty() { | ||||
}; | // Ignore errors when writing to stderr, we’re already exiting | ||||
e.exit() | // with failure code so there’s not much more we can do. | ||||
let _ = | |||||
ui.write_stderr(&format_bytes!(b"abort: {}\n", message)); | |||||
} | } | ||||
exitcode::ABORT | |||||
} | } | ||||
}; | |||||
std::process::exit(exit_code) | |||||
} | } | ||||
fn match_subcommand( | fn match_subcommand( | ||||
matches: ArgMatches, | matches: ArgMatches, | ||||
ui: &ui::Ui, | ui: &ui::Ui, | ||||
) -> Result<(), CommandError> { | ) -> Result<(), CommandError> { | ||||
match matches.subcommand() { | match matches.subcommand() { | ||||
("root", _) => commands::root::RootCommand::new().run(&ui), | ("root", _) => commands::root::RootCommand::new().run(&ui), |
Unwritable file descriptor | Unwritable file descriptor | ||||
$ rhg root > /dev/full | $ rhg root > /dev/full | ||||
abort: No space left on device (os error 28) | abort: No space left on device (os error 28) | ||||
[255] | [255] | ||||
Deleted repository | Deleted repository | ||||
$ rm -rf `pwd` | $ rm -rf `pwd` | ||||
$ rhg root | $ rhg root | ||||
abort: error getting current working directory: $ENOENT$ | abort: $ENOENT$: current directory | ||||
[255] | [255] | ||||
Listing tracked files | Listing tracked files | ||||
$ cd $TESTTMP | $ cd $TESTTMP | ||||
$ hg init repository | $ hg init repository | ||||
$ cd repository | $ cd repository | ||||
$ for i in 1 2 3; do | $ for i in 1 2 3; do | ||||
> echo $i >> file$i | > echo $i >> file$i | ||||
generaldelta | generaldelta | ||||
revlogv1 | revlogv1 | ||||
sparserevlog | sparserevlog | ||||
store | store | ||||
indoor-pool | indoor-pool | ||||
$ echo -e '\xFF' >> .hg/requires | $ echo -e '\xFF' >> .hg/requires | ||||
$ rhg debugrequirements | $ rhg debugrequirements | ||||
corrupted repository: parse error in 'requires' file | abort: corrupted repository: parse error in 'requires' file | ||||
[255] | [255] | ||||
Persistent nodemap | Persistent nodemap | ||||
$ cd $TESTTMP | $ cd $TESTTMP | ||||
$ rm -rf repository | $ rm -rf repository | ||||
$ hg init repository | $ hg init repository | ||||
$ cd repository | $ cd repository | ||||
$ rhg debugrequirements | grep nodemap | $ rhg debugrequirements | grep nodemap |