diff --git a/rust/Cargo.lock b/rust/Cargo.lock --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -489,6 +489,9 @@ [[package]] name = "rhg" version = "0.1.0" +dependencies = [ + "hg-core 0.1.0", +] [[package]] name = "rustc_version" diff --git a/rust/rhg/Cargo.toml b/rust/rhg/Cargo.toml --- a/rust/rhg/Cargo.toml +++ b/rust/rhg/Cargo.toml @@ -5,4 +5,5 @@ edition = "2018" [dependencies] +hg-core = { path = "../hg-core"} diff --git a/rust/rhg/src/commands.rs b/rust/rhg/src/commands.rs --- a/rust/rhg/src/commands.rs +++ b/rust/rhg/src/commands.rs @@ -1,3 +1,4 @@ +pub mod root; use crate::error::CommandError; /// The common trait for rhg commands diff --git a/rust/rhg/src/commands/root.rs b/rust/rhg/src/commands/root.rs new file mode 100644 --- /dev/null +++ b/rust/rhg/src/commands/root.rs @@ -0,0 +1,76 @@ +use crate::commands::Command; +use crate::error::{CommandError, CommandErrorKind}; +use crate::ui::Ui; +use hg::operations::{FindRoot, FindRootError, FindRootErrorKind, Operation}; +use hg::utils::files::get_bytes_from_path; +use std::path::PathBuf; + +pub const HELP_TEXT: &str = " +Print the root directory of the current repository. + +Returns 0 on success. +"; + +pub struct RootCommand { + ui: Ui, +} + +impl RootCommand { + pub fn new() -> Self { + RootCommand { ui: Ui::new() } + } + + fn display_found_path( + &self, + path_buf: PathBuf, + ) -> Result<(), CommandError> { + let bytes = get_bytes_from_path(path_buf); + + // TODO use formating macro + self.ui.write_stdout(&[bytes.as_slice(), b"\n"].concat())?; + + Err(CommandErrorKind::Ok.into()) + } + + fn display_error(&self, error: FindRootError) -> Result<(), CommandError> { + match error.kind { + FindRootErrorKind::RootNotFound(path) => { + let bytes = get_bytes_from_path(path); + + // TODO use formating macro + self.ui.write_stderr( + &[ + b"abort: no repository found in '", + bytes.as_slice(), + b"' (.hg not found)!\n", + ] + .concat(), + )?; + + Err(CommandErrorKind::RootNotFound.into()) + } + FindRootErrorKind::GetCurrentDirError(e) => { + // TODO use formating macro + self.ui.write_stderr( + &[ + b"abort: error getting current working directory: ", + e.to_string().as_bytes(), + b"\n", + ] + .concat(), + )?; + + Err(CommandErrorKind::CurrentDirNotFound.into()) + } + } + } +} + +impl Command for RootCommand { + fn run(&self) -> Result<(), CommandError> { + match FindRoot::new().run() { + Ok(path_buf) => self.display_found_path(path_buf), + Err(e) => self.display_error(e), + } + } +} diff --git a/rust/rhg/src/error.rs b/rust/rhg/src/error.rs --- a/rust/rhg/src/error.rs +++ b/rust/rhg/src/error.rs @@ -1,3 +1,60 @@ +use crate::exitcode; +use crate::ui::UiError; +use std::convert::From; + +/// The kind of command error +#[derive(Debug, PartialEq)] +pub enum CommandErrorKind { + /// The command finished without error + Ok, + /// The root of the repository cannot be found + RootNotFound, + /// The current directory cannot be found + CurrentDirNotFound, + /// The standard output stream cannot be written to + StdoutError, + /// The standard error stream cannot be written to + StderrError, +} + +impl CommandErrorKind { + pub fn get_exit_code(&self) -> exitcode::ExitCode { + match self { + CommandErrorKind::Ok => exitcode::OK, + CommandErrorKind::RootNotFound => exitcode::ABORT, + CommandErrorKind::CurrentDirNotFound => exitcode::ABORT, + CommandErrorKind::StdoutError => exitcode::ABORT, + CommandErrorKind::StderrError => exitcode::ABORT, + } + } +} + /// The error type for the Command trait #[derive(Debug, PartialEq)] -pub struct CommandError {} +pub struct CommandError { + pub kind: CommandErrorKind, +} + +impl CommandError { + /// Exist the process with the corresponding exit code. + pub fn exit(&self) -> () { + std::process::exit(self.kind.get_exit_code()) + } +} + +impl From for CommandError { + fn from(kind: CommandErrorKind) -> Self { + CommandError { kind } + } +} + +impl From for CommandError { + fn from(error: UiError) -> Self { + CommandError { + kind: match error { + UiError::StdoutError(_) => CommandErrorKind::StdoutError, + UiError::StderrError(_) => CommandErrorKind::StderrError, + }, + } + } +} diff --git a/rust/rhg/src/exitcode.rs b/rust/rhg/src/exitcode.rs --- a/rust/rhg/src/exitcode.rs +++ b/rust/rhg/src/exitcode.rs @@ -1,4 +1,10 @@ pub type ExitCode = i32; +/// Successful exit +pub const OK: ExitCode = 0; + +/// Generic abort +pub const ABORT: ExitCode = 255; + /// Command not implemented by rhg pub const UNIMPLEMENTED_COMMAND: ExitCode = 252; diff --git a/rust/rhg/src/main.rs b/rust/rhg/src/main.rs --- a/rust/rhg/src/main.rs +++ b/rust/rhg/src/main.rs @@ -1,6 +1,7 @@ mod commands; mod error; mod exitcode; +mod ui; fn main() { std::process::exit(exitcode::UNIMPLEMENTED_COMMAND) diff --git a/rust/rhg/src/ui.rs b/rust/rhg/src/ui.rs new file mode 100644 --- /dev/null +++ b/rust/rhg/src/ui.rs @@ -0,0 +1,54 @@ +use std::io; +use std::io::Write; + +pub struct Ui {} + +/// The kind of user interface error +pub enum UiError { + /// The standard output stream cannot be written to + StdoutError(io::Error), + /// The standard error stream cannot be written to + StderrError(io::Error), +} + +/// The commandline user interface +impl Ui { + pub fn new() -> Self { + Ui {} + } + + /// Write bytes to stdout + pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> { + let mut stdout = io::stdout(); + + self.write_stream(&mut stdout, bytes) + .or_else(|e| self.into_stdout_error(e))?; + + stdout.flush().or_else(|e| self.into_stdout_error(e)) + } + + fn into_stdout_error(&self, error: io::Error) -> Result<(), UiError> { + self.write_stderr( + &[b"abort: ", error.to_string().as_bytes(), b"\n"].concat(), + )?; + Err(UiError::StdoutError(error)) + } + + /// Write bytes to stderr + pub fn write_stderr(&self, bytes: &[u8]) -> Result<(), UiError> { + let mut stderr = io::stderr(); + + self.write_stream(&mut stderr, bytes) + .or_else(|e| Err(UiError::StderrError(e)))?; + + stderr.flush().or_else(|e| Err(UiError::StderrError(e))) + } + + fn write_stream( + &self, + stream: &mut impl Write, + bytes: &[u8], + ) -> Result<(), io::Error> { + stream.write_all(bytes) + } +}