diff --git a/rust/hg-core/src/errors.rs b/rust/hg-core/src/errors.rs --- a/rust/hg-core/src/errors.rs +++ b/rust/hg-core/src/errors.rs @@ -40,6 +40,10 @@ // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html HgError::CorruptedRepository(explanation.into()) } + + pub fn unsupported(explanation: impl Into) -> Self { + HgError::UnsupportedFeature(explanation.into()) + } } // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly? diff --git a/rust/hg-core/src/repo.rs b/rust/hg-core/src/repo.rs --- a/rust/hg-core/src/repo.rs +++ b/rust/hg-core/src/repo.rs @@ -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, } #[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 { + 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 { + &self.requirements + } + /// For accessing repository files (in `.hg`), except for the store /// (`.hg/store`). pub(crate) fn hg_vfs(&self) -> Vfs<'_> { diff --git a/rust/hg-core/src/requirements.rs b/rust/hg-core/src/requirements.rs --- a/rust/hg-core/src/requirements.rs +++ b/rust/hg-core/src/requirements.rs @@ -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, HgError> { +fn parse(bytes: &[u8]) -> Result, 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, 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, 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"; 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 @@ -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())?; diff --git a/tests/test-rhg.t b/tests/test-rhg.t --- a/tests/test-rhg.t +++ b/tests/test-rhg.t @@ -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