requirements: move loading to hg-core and add parsing
authorSimon Sapin <simon-commits@exyr.org>
Tue, 24 Nov 2020 17:49:16 +0100
changeset 45924 a2eda1ff22aa
parent 45923 ead435aa5294
child 45925 6aacc39501f7
requirements: move loading to hg-core and add parsing No functional change, checking comes later. Differential Revision: https://phab.mercurial-scm.org/D9398
rust/hg-core/src/lib.rs
rust/hg-core/src/requirements.rs
rust/rhg/src/commands/debugrequirements.rs
rust/rhg/src/error.rs
--- a/rust/hg-core/src/lib.rs	Tue Nov 24 15:11:58 2020 +0100
+++ b/rust/hg-core/src/lib.rs	Tue Nov 24 17:49:16 2020 +0100
@@ -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},
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/requirements.rs	Tue Nov 24 17:49:16 2020 +0100
@@ -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’t support
+    Unsupported {
+        feature: String,
+    },
+}
+
+fn parse(bytes: &[u8]) -> Result<Vec<String>, ()> {
+    // 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<Vec<String>, 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))?,
+    }
+}
--- a/rust/rhg/src/commands/debugrequirements.rs	Tue Nov 24 15:11:58 2020 +0100
+++ b/rust/rhg/src/commands/debugrequirements.rs	Tue Nov 24 17:49:16 2020 +0100
@@ -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(())
     }
 }
--- a/rust/rhg/src/error.rs	Tue Nov 24 15:11:58 2020 +0100
+++ b/rust/rhg/src/error.rs	Tue Nov 24 17:49:16 2020 +0100
@@ -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<RequirementsError> for CommandError {
+    fn from(err: RequirementsError) -> Self {
+        CommandError {
+            kind: CommandErrorKind::RequirementsError(err),
+        }
+    }
+}