diff --git a/rust/hg-core/src/lib.rs b/rust/hg-core/src/lib.rs --- a/rust/hg-core/src/lib.rs +++ b/rust/hg-core/src/lib.rs @@ -8,6 +8,7 @@ pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors}; mod dirstate; pub mod discovery; +pub mod requirements; pub mod testing; // unconditionally built, for use from integration tests pub use dirstate::{ dirs_multiset::{DirsMultiset, DirsMultisetIter}, diff --git a/rust/hg-core/src/requirements.rs b/rust/hg-core/src/requirements.rs new file mode 100644 --- /dev/null +++ b/rust/hg-core/src/requirements.rs @@ -0,0 +1,53 @@ +use std::io; +use std::path::Path; + +#[derive(Debug)] +pub enum RequirementsError { + // TODO: include a path? + Io(io::Error), + /// The `requires` file is corrupted + Corrupted, + /// The repository requires a feature that we don稚 support + Unsupported { + feature: String, + }, +} + +fn parse(bytes: &[u8]) -> Result, ()> { + // The Python code reading this file uses `str.splitlines` + // which looks for a number of line separators (even including a couple of + // non-ASCII ones), but Python code writing it always uses `\n`. + let lines = bytes.split(|&byte| byte == b'\n'); + + lines + .filter(|line| !line.is_empty()) + .map(|line| { + // Python uses Unicode `str.isalnum` but feature names are all + // ASCII + if line[0].is_ascii_alphanumeric() { + Ok(String::from_utf8(line.into()).unwrap()) + } else { + Err(()) + } + }) + .collect() +} + +pub fn load(repo_root: &Path) -> Result, RequirementsError> { + match std::fs::read(repo_root.join(".hg").join("requires")) { + Ok(bytes) => parse(&bytes).map_err(|()| RequirementsError::Corrupted), + + // Treat a missing file the same as an empty file. + // From `mercurial/localrepo.py`: + // > requires file contains a newline-delimited list of + // > features/capabilities the opener (us) must have in order to use + // > the repository. This file was introduced in Mercurial 0.9.2, + // > which means very old repositories may not have one. We assume + // > a missing file translates to no requirements. + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + Ok(Vec::new()) + } + + Err(error) => Err(RequirementsError::Io(error))?, + } +} diff --git a/rust/rhg/src/commands/debugrequirements.rs b/rust/rhg/src/commands/debugrequirements.rs --- a/rust/rhg/src/commands/debugrequirements.rs +++ b/rust/rhg/src/commands/debugrequirements.rs @@ -1,7 +1,8 @@ use crate::commands::Command; -use crate::error::{CommandError, CommandErrorKind}; +use crate::error::CommandError; use crate::ui::Ui; use hg::operations::FindRoot; +use hg::requirements; pub const HELP_TEXT: &str = " Print the current repo requirements. @@ -18,23 +19,12 @@ impl Command for DebugRequirementsCommand { fn run(&self, ui: &Ui) -> Result<(), CommandError> { let root = FindRoot::new().run()?; - let requires = root.join(".hg").join("requires"); - let requirements = match std::fs::read(requires) { - Ok(bytes) => bytes, - - // Treat a missing file the same as an empty file. - // From `mercurial/localrepo.py`: - // > requires file contains a newline-delimited list of - // > features/capabilities the opener (us) must have in order to use - // > the repository. This file was introduced in Mercurial 0.9.2, - // > which means very old repositories may not have one. We assume - // > a missing file translates to no requirements. - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Vec::new(), - - Err(error) => Err(CommandErrorKind::FileError(error))?, - }; - - ui.write_stdout(&requirements)?; + let mut output = String::new(); + for req in requirements::load(&root)? { + output.push_str(&req); + output.push('\n'); + } + ui.write_stdout(output.as_bytes())?; Ok(()) } } 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,6 +1,7 @@ use crate::exitcode; use crate::ui::UiError; use hg::operations::{FindRootError, FindRootErrorKind}; +use hg::requirements::RequirementsError; use hg::utils::files::get_bytes_from_path; use std::convert::From; use std::path::PathBuf; @@ -12,9 +13,8 @@ RootNotFound(PathBuf), /// The current directory cannot be found CurrentDirNotFound(std::io::Error), - /// Error while reading or writing a file - // TODO: add the file name/path? - FileError(std::io::Error), + /// `.hg/requires` + RequirementsError(RequirementsError), /// The standard output stream cannot be written to StdoutError, /// The standard error stream cannot be written to @@ -30,7 +30,7 @@ match self { CommandErrorKind::RootNotFound(_) => exitcode::ABORT, CommandErrorKind::CurrentDirNotFound(_) => exitcode::ABORT, - CommandErrorKind::FileError(_) => exitcode::ABORT, + CommandErrorKind::RequirementsError(_) => exitcode::ABORT, CommandErrorKind::StdoutError => exitcode::ABORT, CommandErrorKind::StderrError => exitcode::ABORT, CommandErrorKind::Abort(_) => exitcode::ABORT, @@ -62,6 +62,11 @@ ] .concat(), ), + CommandErrorKind::RequirementsError( + RequirementsError::Corrupted, + ) => Some( + "abort: .hg/requires is corrupted\n".as_bytes().to_owned(), + ), CommandErrorKind::Abort(message) => message.to_owned(), _ => None, } @@ -115,3 +120,11 @@ } } } + +impl From for CommandError { + fn from(err: RequirementsError) -> Self { + CommandError { + kind: CommandErrorKind::RequirementsError(err), + } + } +}