rhg: initial support for shared repositories
authorSimon Sapin <simon.sapin@octobus.net>
Thu, 14 Jan 2021 13:04:12 +0100
changeset 46462 d03b0601e0eb
parent 46461 f3f4d1b7dc97
child 46463 95b276283b67
rhg: initial support for shared repositories Differential Revision: https://phab.mercurial-scm.org/D9941
rust/hg-core/src/errors.rs
rust/hg-core/src/repo.rs
rust/hg-core/src/requirements.rs
rust/rhg/src/commands/debugrequirements.rs
tests/test-rhg.t
--- a/rust/hg-core/src/errors.rs	Mon Feb 01 19:30:28 2021 +0100
+++ b/rust/hg-core/src/errors.rs	Thu Jan 14 13:04:12 2021 +0100
@@ -40,6 +40,10 @@
         // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
         HgError::CorruptedRepository(explanation.into())
     }
+
+    pub fn unsupported(explanation: impl Into<String>) -> Self {
+        HgError::UnsupportedFeature(explanation.into())
+    }
 }
 
 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
--- a/rust/hg-core/src/repo.rs	Mon Feb 01 19:30:28 2021 +0100
+++ b/rust/hg-core/src/repo.rs	Thu Jan 14 13:04:12 2021 +0100
@@ -1,6 +1,8 @@
 use crate::errors::{HgError, IoResultExt};
 use crate::requirements;
+use crate::utils::files::get_path_from_bytes;
 use memmap::{Mmap, MmapOptions};
+use std::collections::HashSet;
 use std::path::{Path, PathBuf};
 
 /// A repository on disk
@@ -8,6 +10,7 @@
     working_directory: PathBuf,
     dot_hg: PathBuf,
     store: PathBuf,
+    requirements: HashSet<String>,
 }
 
 #[derive(Debug, derive_more::From)]
@@ -32,15 +35,8 @@
         let current_directory = crate::utils::current_dir()?;
         // ancestors() is inclusive: it first yields `current_directory` as-is.
         for ancestor in current_directory.ancestors() {
-            let dot_hg = ancestor.join(".hg");
-            if dot_hg.is_dir() {
-                let repo = Self {
-                    store: dot_hg.join("store"),
-                    dot_hg,
-                    working_directory: ancestor.to_owned(),
-                };
-                requirements::check(&repo)?;
-                return Ok(repo);
+            if ancestor.join(".hg").is_dir() {
+                return Ok(Self::new_at_path(ancestor.to_owned())?);
             }
         }
         Err(RepoFindError::NotFoundInCurrentDirectoryOrAncestors {
@@ -48,10 +44,54 @@
         })
     }
 
+    /// To be called after checking that `.hg` is a sub-directory
+    fn new_at_path(working_directory: PathBuf) -> Result<Self, HgError> {
+        let dot_hg = working_directory.join(".hg");
+        let hg_vfs = Vfs { base: &dot_hg };
+        let reqs = requirements::load_if_exists(hg_vfs)?;
+        let relative =
+            reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
+        let shared =
+            reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
+        let store_path;
+        if !shared {
+            store_path = dot_hg.join("store");
+        } else {
+            let bytes = hg_vfs.read("sharedpath")?;
+            let mut shared_path = get_path_from_bytes(&bytes).to_owned();
+            if relative {
+                shared_path = dot_hg.join(shared_path)
+            }
+            if !shared_path.is_dir() {
+                return Err(HgError::corrupted(format!(
+                    ".hg/sharedpath points to nonexistent directory {}",
+                    shared_path.display()
+                )));
+            }
+
+            store_path = shared_path.join("store");
+        }
+
+        let repo = Self {
+            requirements: reqs,
+            working_directory,
+            store: store_path,
+            dot_hg,
+        };
+
+        requirements::check(&repo)?;
+
+        Ok(repo)
+    }
+
     pub fn working_directory_path(&self) -> &Path {
         &self.working_directory
     }
 
+    pub fn requirements(&self) -> &HashSet<String> {
+        &self.requirements
+    }
+
     /// For accessing repository files (in `.hg`), except for the store
     /// (`.hg/store`).
     pub(crate) fn hg_vfs(&self) -> Vfs<'_> {
--- a/rust/hg-core/src/requirements.rs	Mon Feb 01 19:30:28 2021 +0100
+++ b/rust/hg-core/src/requirements.rs	Thu Jan 14 13:04:12 2021 +0100
@@ -1,7 +1,8 @@
 use crate::errors::{HgError, HgResultExt};
-use crate::repo::Repo;
+use crate::repo::{Repo, Vfs};
+use std::collections::HashSet;
 
-fn parse(bytes: &[u8]) -> Result<Vec<String>, HgError> {
+fn parse(bytes: &[u8]) -> Result<HashSet<String>, HgError> {
     // 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`.
@@ -21,10 +22,8 @@
         .collect()
 }
 
-pub fn load(repo: &Repo) -> Result<Vec<String>, HgError> {
-    if let Some(bytes) =
-        repo.hg_vfs().read("requires").io_not_found_as_none()?
-    {
+pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
+    if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? {
         parse(&bytes)
     } else {
         // Treat a missing file the same as an empty file.
@@ -34,13 +33,13 @@
         // > 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.
-        Ok(Vec::new())
+        Ok(HashSet::new())
     }
 }
 
-pub fn check(repo: &Repo) -> Result<(), HgError> {
-    for feature in load(repo)? {
-        if !SUPPORTED.contains(&&*feature) {
+pub(crate) fn check(repo: &Repo) -> Result<(), HgError> {
+    for feature in repo.requirements() {
+        if !SUPPORTED.contains(&feature.as_str()) {
             // TODO: collect and all unknown features and include them in the
             // error message?
             return Err(HgError::UnsupportedFeature(format!(
@@ -58,10 +57,77 @@
     "fncache",
     "generaldelta",
     "revlogv1",
-    "sparserevlog",
+    SHARED_REQUIREMENT,
+    SPARSEREVLOG_REQUIREMENT,
+    RELATIVE_SHARED_REQUIREMENT,
     "store",
     // As of this writing everything rhg does is read-only.
     // When it starts writing to the repository, it’ll need to either keep the
     // persistent nodemap up to date or remove this entry:
     "persistent-nodemap",
 ];
+
+// Copied from mercurial/requirements.py:
+
+/// When narrowing is finalized and no longer subject to format changes,
+/// we should move this to just "narrow" or similar.
+#[allow(unused)]
+pub(crate) const NARROW_REQUIREMENT: &str = "narrowhg-experimental";
+
+/// Enables sparse working directory usage
+#[allow(unused)]
+pub(crate) const SPARSE_REQUIREMENT: &str = "exp-sparse";
+
+/// Enables the internal phase which is used to hide changesets instead
+/// of stripping them
+#[allow(unused)]
+pub(crate) const INTERNAL_PHASE_REQUIREMENT: &str = "internal-phase";
+
+/// Stores manifest in Tree structure
+#[allow(unused)]
+pub(crate) const TREEMANIFEST_REQUIREMENT: &str = "treemanifest";
+
+/// Increment the sub-version when the revlog v2 format changes to lock out old
+/// clients.
+#[allow(unused)]
+pub(crate) const REVLOGV2_REQUIREMENT: &str = "exp-revlogv2.1";
+
+/// A repository with the sparserevlog feature will have delta chains that
+/// can spread over a larger span. Sparse reading cuts these large spans into
+/// pieces, so that each piece isn't too big.
+/// Without the sparserevlog capability, reading from the repository could use
+/// huge amounts of memory, because the whole span would be read at once,
+/// including all the intermediate revisions that aren't pertinent for the
+/// chain. This is why once a repository has enabled sparse-read, it becomes
+/// required.
+#[allow(unused)]
+pub(crate) const SPARSEREVLOG_REQUIREMENT: &str = "sparserevlog";
+
+/// A repository with the sidedataflag requirement will allow to store extra
+/// information for revision without altering their original hashes.
+#[allow(unused)]
+pub(crate) const SIDEDATA_REQUIREMENT: &str = "exp-sidedata-flag";
+
+/// A repository with the the copies-sidedata-changeset requirement will store
+/// copies related information in changeset's sidedata.
+#[allow(unused)]
+pub(crate) const COPIESSDC_REQUIREMENT: &str = "exp-copies-sidedata-changeset";
+
+/// The repository use persistent nodemap for the changelog and the manifest.
+#[allow(unused)]
+pub(crate) const NODEMAP_REQUIREMENT: &str = "persistent-nodemap";
+
+/// Denotes that the current repository is a share
+#[allow(unused)]
+pub(crate) const SHARED_REQUIREMENT: &str = "shared";
+
+/// Denotes that current repository is a share and the shared source path is
+/// relative to the current repository root path
+#[allow(unused)]
+pub(crate) const RELATIVE_SHARED_REQUIREMENT: &str = "relshared";
+
+/// A repository with share implemented safely. The repository has different
+/// store and working copy requirements i.e. both `.hg/requires` and
+/// `.hg/store/requires` are present.
+#[allow(unused)]
+pub(crate) const SHARESAFE_REQUIREMENT: &str = "exp-sharesafe";
--- a/rust/rhg/src/commands/debugrequirements.rs	Mon Feb 01 19:30:28 2021 +0100
+++ b/rust/rhg/src/commands/debugrequirements.rs	Thu Jan 14 13:04:12 2021 +0100
@@ -2,7 +2,6 @@
 use crate::error::CommandError;
 use crate::ui::Ui;
 use hg::repo::Repo;
-use hg::requirements;
 
 pub const HELP_TEXT: &str = "
 Print the current repo requirements.
@@ -20,8 +19,10 @@
     fn run(&self, ui: &Ui) -> Result<(), CommandError> {
         let repo = Repo::find()?;
         let mut output = String::new();
-        for req in requirements::load(&repo)? {
-            output.push_str(&req);
+        let mut requirements: Vec<_> = repo.requirements().iter().collect();
+        requirements.sort();
+        for req in requirements {
+            output.push_str(req);
             output.push('\n');
         }
         ui.write_stdout(output.as_bytes())?;
--- a/tests/test-rhg.t	Mon Feb 01 19:30:28 2021 +0100
+++ b/tests/test-rhg.t	Thu Jan 14 13:04:12 2021 +0100
@@ -218,9 +218,9 @@
 
   $ cd repo2
   $ rhg files
-  [252]
+  a
   $ rhg cat -r 0 a
-  [252]
+  a
 
 Same with relative sharing
 
@@ -231,9 +231,9 @@
 
   $ cd repo3
   $ rhg files
-  [252]
+  a
   $ rhg cat -r 0 a
-  [252]
+  a
 
 Same with share-safe