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 | ||||