Details
Details
- Reviewers
- None
- Group Reviewers
hg-reviewers - Commits
- rHGd03b0601e0eb: rhg: initial support for shared repositories
Diff Detail
Diff Detail
- Repository
- rHG Mercurial
- Branch
- default
- Lint
No Linters Available - Unit
No Unit Test Coverage
( )
| hg-reviewers |
| No Linters Available |
| No Unit Test Coverage |
| Path | Packages | |||
|---|---|---|---|---|
| M | rust/hg-core/src/errors.rs (4 lines) | |||
| M | rust/hg-core/src/repo.rs (58 lines) | |||
| M | rust/hg-core/src/requirements.rs (88 lines) | |||
| M | rust/rhg/src/commands/debugrequirements.rs (7 lines) | |||
| M | tests/test-rhg.t (8 lines) |
| Commit | Parents | Author | Summary | Date |
|---|---|---|---|---|
| 228b75c6a619 | 5ce4ceb43a88 | Simon Sapin | Jan 14 2021, 7:04 AM |
| Status | Author | Revision | |
|---|---|---|---|
| Closed | SimonSapin | ||
| Closed | SimonSapin | ||
| Closed | SimonSapin |
| impl HgError { | impl HgError { | ||||
| pub fn corrupted(explanation: impl Into<String>) -> Self { | pub fn corrupted(explanation: impl Into<String>) -> Self { | ||||
| // TODO: capture a backtrace here and keep it in the error value | // TODO: capture a backtrace here and keep it in the error value | ||||
| // to aid debugging? | // to aid debugging? | ||||
| // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html | // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html | ||||
| HgError::CorruptedRepository(explanation.into()) | 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? | // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly? | ||||
| impl fmt::Display for HgError { | impl fmt::Display for HgError { | ||||
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||||
| match self { | match self { | ||||
| HgError::IoError { error, context } => { | HgError::IoError { error, context } => { | ||||
| write!(f, "{}: {}", error, context) | write!(f, "{}: {}", error, context) | ||||
| use crate::errors::{HgError, IoResultExt}; | use crate::errors::{HgError, IoResultExt}; | ||||
| use crate::requirements; | use crate::requirements; | ||||
| use crate::utils::files::get_path_from_bytes; | |||||
| use memmap::{Mmap, MmapOptions}; | use memmap::{Mmap, MmapOptions}; | ||||
| use std::collections::HashSet; | |||||
| use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||
| /// A repository on disk | /// A repository on disk | ||||
| pub struct Repo { | pub struct Repo { | ||||
| working_directory: PathBuf, | working_directory: PathBuf, | ||||
| dot_hg: PathBuf, | dot_hg: PathBuf, | ||||
| store: PathBuf, | store: PathBuf, | ||||
| requirements: HashSet<String>, | |||||
| } | } | ||||
| #[derive(Debug, derive_more::From)] | #[derive(Debug, derive_more::From)] | ||||
| pub enum RepoFindError { | pub enum RepoFindError { | ||||
| NotFoundInCurrentDirectoryOrAncestors { | NotFoundInCurrentDirectoryOrAncestors { | ||||
| current_directory: PathBuf, | current_directory: PathBuf, | ||||
| }, | }, | ||||
| #[from] | #[from] | ||||
| Other(HgError), | Other(HgError), | ||||
| } | } | ||||
| /// Filesystem access abstraction for the contents of a given "base" diretory | /// Filesystem access abstraction for the contents of a given "base" diretory | ||||
| #[derive(Clone, Copy)] | #[derive(Clone, Copy)] | ||||
| pub(crate) struct Vfs<'a> { | pub(crate) struct Vfs<'a> { | ||||
| base: &'a Path, | base: &'a Path, | ||||
| } | } | ||||
| impl Repo { | impl Repo { | ||||
| /// Search the current directory and its ancestores for a repository: | /// Search the current directory and its ancestores for a repository: | ||||
| /// a working directory that contains a `.hg` sub-directory. | /// a working directory that contains a `.hg` sub-directory. | ||||
| pub fn find() -> Result<Self, RepoFindError> { | pub fn find() -> Result<Self, RepoFindError> { | ||||
| let current_directory = crate::utils::current_dir()?; | let current_directory = crate::utils::current_dir()?; | ||||
| // ancestors() is inclusive: it first yields `current_directory` as-is. | // ancestors() is inclusive: it first yields `current_directory` as-is. | ||||
| for ancestor in current_directory.ancestors() { | for ancestor in current_directory.ancestors() { | ||||
| let dot_hg = ancestor.join(".hg"); | if ancestor.join(".hg").is_dir() { | ||||
| if dot_hg.is_dir() { | return Ok(Self::new_at_path(ancestor.to_owned())?); | ||||
| let repo = Self { | |||||
| store: dot_hg.join("store"), | |||||
| dot_hg, | |||||
| working_directory: ancestor.to_owned(), | |||||
| }; | |||||
| requirements::check(&repo)?; | |||||
| return Ok(repo); | |||||
| } | } | ||||
| } | } | ||||
| Err(RepoFindError::NotFoundInCurrentDirectoryOrAncestors { | Err(RepoFindError::NotFoundInCurrentDirectoryOrAncestors { | ||||
| current_directory, | current_directory, | ||||
| }) | }) | ||||
| } | } | ||||
| /// 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 { | pub fn working_directory_path(&self) -> &Path { | ||||
| &self.working_directory | &self.working_directory | ||||
| } | } | ||||
| pub fn requirements(&self) -> &HashSet<String> { | |||||
| &self.requirements | |||||
| } | |||||
| /// For accessing repository files (in `.hg`), except for the store | /// For accessing repository files (in `.hg`), except for the store | ||||
| /// (`.hg/store`). | /// (`.hg/store`). | ||||
| pub(crate) fn hg_vfs(&self) -> Vfs<'_> { | pub(crate) fn hg_vfs(&self) -> Vfs<'_> { | ||||
| Vfs { base: &self.dot_hg } | Vfs { base: &self.dot_hg } | ||||
| } | } | ||||
| /// For accessing repository store files (in `.hg/store`) | /// For accessing repository store files (in `.hg/store`) | ||||
| pub(crate) fn store_vfs(&self) -> Vfs<'_> { | pub(crate) fn store_vfs(&self) -> Vfs<'_> { | ||||
| use crate::errors::{HgError, HgResultExt}; | 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` | // The Python code reading this file uses `str.splitlines` | ||||
| // which looks for a number of line separators (even including a couple of | // which looks for a number of line separators (even including a couple of | ||||
| // non-ASCII ones), but Python code writing it always uses `\n`. | // non-ASCII ones), but Python code writing it always uses `\n`. | ||||
| let lines = bytes.split(|&byte| byte == b'\n'); | let lines = bytes.split(|&byte| byte == b'\n'); | ||||
| lines | lines | ||||
| .filter(|line| !line.is_empty()) | .filter(|line| !line.is_empty()) | ||||
| .map(|line| { | .map(|line| { | ||||
| // Python uses Unicode `str.isalnum` but feature names are all | // Python uses Unicode `str.isalnum` but feature names are all | ||||
| // ASCII | // ASCII | ||||
| if line[0].is_ascii_alphanumeric() && line.is_ascii() { | if line[0].is_ascii_alphanumeric() && line.is_ascii() { | ||||
| Ok(String::from_utf8(line.into()).unwrap()) | Ok(String::from_utf8(line.into()).unwrap()) | ||||
| } else { | } else { | ||||
| Err(HgError::corrupted("parse error in 'requires' file")) | Err(HgError::corrupted("parse error in 'requires' file")) | ||||
| } | } | ||||
| }) | }) | ||||
| .collect() | .collect() | ||||
| } | } | ||||
| pub fn load(repo: &Repo) -> Result<Vec<String>, HgError> { | pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> { | ||||
| if let Some(bytes) = | if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? { | ||||
| repo.hg_vfs().read("requires").io_not_found_as_none()? | |||||
| { | |||||
| parse(&bytes) | parse(&bytes) | ||||
| } else { | } else { | ||||
| // Treat a missing file the same as an empty file. | // Treat a missing file the same as an empty file. | ||||
| // From `mercurial/localrepo.py`: | // From `mercurial/localrepo.py`: | ||||
| // > requires file contains a newline-delimited list of | // > requires file contains a newline-delimited list of | ||||
| // > features/capabilities the opener (us) must have in order to use | // > features/capabilities the opener (us) must have in order to use | ||||
| // > the repository. This file was introduced in Mercurial 0.9.2, | // > the repository. This file was introduced in Mercurial 0.9.2, | ||||
| // > which means very old repositories may not have one. We assume | // > which means very old repositories may not have one. We assume | ||||
| // > a missing file translates to no requirements. | // > a missing file translates to no requirements. | ||||
| Ok(Vec::new()) | Ok(HashSet::new()) | ||||
| } | } | ||||
| } | } | ||||
| pub fn check(repo: &Repo) -> Result<(), HgError> { | pub(crate) fn check(repo: &Repo) -> Result<(), HgError> { | ||||
| for feature in load(repo)? { | for feature in repo.requirements() { | ||||
| if !SUPPORTED.contains(&&*feature) { | if !SUPPORTED.contains(&feature.as_str()) { | ||||
| // TODO: collect and all unknown features and include them in the | // TODO: collect and all unknown features and include them in the | ||||
| // error message? | // error message? | ||||
| return Err(HgError::UnsupportedFeature(format!( | return Err(HgError::UnsupportedFeature(format!( | ||||
| "repository requires feature unknown to this Mercurial: {}", | "repository requires feature unknown to this Mercurial: {}", | ||||
| feature | feature | ||||
| ))); | ))); | ||||
| } | } | ||||
| } | } | ||||
| Ok(()) | Ok(()) | ||||
| } | } | ||||
| // TODO: set this to actually-supported features | // TODO: set this to actually-supported features | ||||
| const SUPPORTED: &[&str] = &[ | const SUPPORTED: &[&str] = &[ | ||||
| "dotencode", | "dotencode", | ||||
| "fncache", | "fncache", | ||||
| "generaldelta", | "generaldelta", | ||||
| "revlogv1", | "revlogv1", | ||||
| "sparserevlog", | SHARED_REQUIREMENT, | ||||
| SPARSEREVLOG_REQUIREMENT, | |||||
| RELATIVE_SHARED_REQUIREMENT, | |||||
| "store", | "store", | ||||
| // As of this writing everything rhg does is read-only. | // As of this writing everything rhg does is read-only. | ||||
| // When it starts writing to the repository, it’ll need to either keep the | // 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 up to date or remove this entry: | ||||
| "persistent-nodemap", | "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"; | |||||
| use crate::commands::Command; | use crate::commands::Command; | ||||
| use crate::error::CommandError; | use crate::error::CommandError; | ||||
| use crate::ui::Ui; | use crate::ui::Ui; | ||||
| use hg::repo::Repo; | use hg::repo::Repo; | ||||
| use hg::requirements; | |||||
| pub const HELP_TEXT: &str = " | pub const HELP_TEXT: &str = " | ||||
| Print the current repo requirements. | Print the current repo requirements. | ||||
| "; | "; | ||||
| pub struct DebugRequirementsCommand {} | pub struct DebugRequirementsCommand {} | ||||
| impl DebugRequirementsCommand { | impl DebugRequirementsCommand { | ||||
| pub fn new() -> Self { | pub fn new() -> Self { | ||||
| DebugRequirementsCommand {} | DebugRequirementsCommand {} | ||||
| } | } | ||||
| } | } | ||||
| impl Command for DebugRequirementsCommand { | impl Command for DebugRequirementsCommand { | ||||
| fn run(&self, ui: &Ui) -> Result<(), CommandError> { | fn run(&self, ui: &Ui) -> Result<(), CommandError> { | ||||
| let repo = Repo::find()?; | let repo = Repo::find()?; | ||||
| let mut output = String::new(); | let mut output = String::new(); | ||||
| for req in requirements::load(&repo)? { | let mut requirements: Vec<_> = repo.requirements().iter().collect(); | ||||
| output.push_str(&req); | requirements.sort(); | ||||
| for req in requirements { | |||||
| output.push_str(req); | |||||
| output.push('\n'); | output.push('\n'); | ||||
| } | } | ||||
| ui.write_stdout(output.as_bytes())?; | ui.write_stdout(output.as_bytes())?; | ||||
| Ok(()) | Ok(()) | ||||
| } | } | ||||
| } | } | ||||
| $ hg share repo1 repo2 | $ hg share repo1 repo2 | ||||
| updating working directory | updating working directory | ||||
| 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | ||||
| And check that basic rhg commands work with sharing | And check that basic rhg commands work with sharing | ||||
| $ cd repo2 | $ cd repo2 | ||||
| $ rhg files | $ rhg files | ||||
| [252] | a | ||||
| $ rhg cat -r 0 a | $ rhg cat -r 0 a | ||||
| [252] | a | ||||
| Same with relative sharing | Same with relative sharing | ||||
| $ cd .. | $ cd .. | ||||
| $ hg share repo2 repo3 --relative | $ hg share repo2 repo3 --relative | ||||
| updating working directory | updating working directory | ||||
| 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | ||||
| $ cd repo3 | $ cd repo3 | ||||
| $ rhg files | $ rhg files | ||||
| [252] | a | ||||
| $ rhg cat -r 0 a | $ rhg cat -r 0 a | ||||
| [252] | a | ||||
| Same with share-safe | Same with share-safe | ||||
| $ echo "[format]" >> $HGRCPATH | $ echo "[format]" >> $HGRCPATH | ||||
| $ echo "use-share-safe = True" >> $HGRCPATH | $ echo "use-share-safe = True" >> $HGRCPATH | ||||
| $ cd $TESTTMP | $ cd $TESTTMP | ||||
| $ hg init repo4 | $ hg init repo4 | ||||