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 |