repo.rs
315 lines
| 10.3 KiB
| application/rls-services+xml
|
RustLexer
Simon Sapin
|
r47215 | use crate::config::{Config, ConfigError, ConfigParseError}; | ||
Simon Sapin
|
r47341 | use crate::errors::{HgError, IoErrorContext, IoResultExt}; | ||
Pulkit Goyal
|
r48199 | use crate::exit_codes; | ||
Simon Sapin
|
r46782 | use crate::requirements; | ||
Simon Sapin
|
r47190 | use crate::utils::files::get_path_from_bytes; | ||
Simon Sapin
|
r47474 | use crate::utils::SliceExt; | ||
Simon Sapin
|
r46782 | use memmap::{Mmap, MmapOptions}; | ||
Simon Sapin
|
r47190 | use std::collections::HashSet; | ||
Simon Sapin
|
r48584 | use std::io::ErrorKind; | ||
Simon Sapin
|
r46782 | use std::path::{Path, PathBuf}; | ||
/// A repository on disk | ||||
pub struct Repo { | ||||
working_directory: PathBuf, | ||||
dot_hg: PathBuf, | ||||
store: PathBuf, | ||||
Simon Sapin
|
r47190 | requirements: HashSet<String>, | ||
Simon Sapin
|
r47215 | config: Config, | ||
Simon Sapin
|
r46782 | } | ||
Simon Sapin
|
r47175 | #[derive(Debug, derive_more::From)] | ||
Simon Sapin
|
r47215 | pub enum RepoError { | ||
NotFound { | ||||
Simon Sapin
|
r47253 | at: PathBuf, | ||
Simon Sapin
|
r47175 | }, | ||
#[from] | ||||
Simon Sapin
|
r47215 | ConfigParseError(ConfigParseError), | ||
#[from] | ||||
Simon Sapin
|
r47175 | Other(HgError), | ||
} | ||||
Simon Sapin
|
r47215 | impl From<ConfigError> for RepoError { | ||
fn from(error: ConfigError) -> Self { | ||||
match error { | ||||
ConfigError::Parse(error) => error.into(), | ||||
ConfigError::Other(error) => error.into(), | ||||
} | ||||
} | ||||
} | ||||
Simon Sapin
|
r46782 | /// Filesystem access abstraction for the contents of a given "base" diretory | ||
#[derive(Clone, Copy)] | ||||
Simon Sapin
|
r47341 | pub struct Vfs<'a> { | ||
pub(crate) base: &'a Path, | ||||
Simon Sapin
|
r46782 | } | ||
impl Repo { | ||||
Pulkit Goyal
|
r48197 | /// tries to find nearest repository root in current working directory or | ||
/// its ancestors | ||||
pub fn find_repo_root() -> Result<PathBuf, RepoError> { | ||||
let current_directory = crate::utils::current_dir()?; | ||||
// ancestors() is inclusive: it first yields `current_directory` | ||||
// as-is. | ||||
for ancestor in current_directory.ancestors() { | ||||
Simon Sapin
|
r48584 | if is_dir(ancestor.join(".hg"))? { | ||
Pulkit Goyal
|
r48197 | return Ok(ancestor.to_path_buf()); | ||
} | ||||
} | ||||
return Err(RepoError::NotFound { | ||||
at: current_directory, | ||||
}); | ||||
} | ||||
Simon Sapin
|
r47255 | /// Find a repository, either at the given path (which must contain a `.hg` | ||
/// sub-directory) or by searching the current directory and its | ||||
/// ancestors. | ||||
Simon Sapin
|
r47253 | /// | ||
Simon Sapin
|
r47255 | /// A method with two very different "modes" like this usually a code smell | ||
/// to make two methods instead, but in this case an `Option` is what rhg | ||||
/// sub-commands get from Clap for the `-R` / `--repository` CLI argument. | ||||
/// Having two methods would just move that `if` to almost all callers. | ||||
Simon Sapin
|
r47253 | pub fn find( | ||
config: &Config, | ||||
Pulkit Goyal
|
r48196 | explicit_path: Option<PathBuf>, | ||
Simon Sapin
|
r47253 | ) -> Result<Self, RepoError> { | ||
if let Some(root) = explicit_path { | ||||
Simon Sapin
|
r48584 | if is_dir(root.join(".hg"))? { | ||
Simon Sapin
|
r47474 | Self::new_at_path(root.to_owned(), config) | ||
Simon Sapin
|
r48584 | } else if is_file(&root)? { | ||
Simon Sapin
|
r47464 | Err(HgError::unsupported("bundle repository").into()) | ||
Simon Sapin
|
r47253 | } else { | ||
Err(RepoError::NotFound { | ||||
at: root.to_owned(), | ||||
}) | ||||
Simon Sapin
|
r47175 | } | ||
Simon Sapin
|
r47253 | } else { | ||
Pulkit Goyal
|
r48197 | let root = Self::find_repo_root()?; | ||
Self::new_at_path(root, config) | ||||
Simon Sapin
|
r46782 | } | ||
} | ||||
Simon Sapin
|
r47190 | /// To be called after checking that `.hg` is a sub-directory | ||
Simon Sapin
|
r47214 | fn new_at_path( | ||
working_directory: PathBuf, | ||||
config: &Config, | ||||
Simon Sapin
|
r47215 | ) -> Result<Self, RepoError> { | ||
Simon Sapin
|
r47190 | let dot_hg = working_directory.join(".hg"); | ||
Simon Sapin
|
r47191 | |||
Simon Sapin
|
r47215 | let mut repo_config_files = Vec::new(); | ||
repo_config_files.push(dot_hg.join("hgrc")); | ||||
repo_config_files.push(dot_hg.join("hgrc-not-shared")); | ||||
Simon Sapin
|
r47190 | let hg_vfs = Vfs { base: &dot_hg }; | ||
Simon Sapin
|
r47191 | let mut reqs = requirements::load_if_exists(hg_vfs)?; | ||
Simon Sapin
|
r47190 | let relative = | ||
reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); | ||||
let shared = | ||||
reqs.contains(requirements::SHARED_REQUIREMENT) || relative; | ||||
Simon Sapin
|
r47191 | |||
// From `mercurial/localrepo.py`: | ||||
// | ||||
// if .hg/requires contains the sharesafe requirement, it means | ||||
// there exists a `.hg/store/requires` too and we should read it | ||||
// NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement | ||||
// is present. We never write SHARESAFE_REQUIREMENT for a repo if store | ||||
// is not present, refer checkrequirementscompat() for that | ||||
// | ||||
// However, if SHARESAFE_REQUIREMENT is not present, it means that the | ||||
// repository was shared the old way. We check the share source | ||||
// .hg/requires for SHARESAFE_REQUIREMENT to detect whether the | ||||
// current repository needs to be reshared | ||||
let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT); | ||||
Simon Sapin
|
r47190 | let store_path; | ||
if !shared { | ||||
store_path = dot_hg.join("store"); | ||||
} else { | ||||
let bytes = hg_vfs.read("sharedpath")?; | ||||
Simon Sapin
|
r47427 | let mut shared_path = | ||
get_path_from_bytes(bytes.trim_end_newlines()).to_owned(); | ||||
Simon Sapin
|
r47190 | if relative { | ||
shared_path = dot_hg.join(shared_path) | ||||
} | ||||
Simon Sapin
|
r48584 | if !is_dir(&shared_path)? { | ||
Simon Sapin
|
r47190 | return Err(HgError::corrupted(format!( | ||
".hg/sharedpath points to nonexistent directory {}", | ||||
shared_path.display() | ||||
Simon Sapin
|
r47215 | )) | ||
.into()); | ||||
Simon Sapin
|
r47190 | } | ||
store_path = shared_path.join("store"); | ||||
Simon Sapin
|
r47191 | |||
let source_is_share_safe = | ||||
requirements::load(Vfs { base: &shared_path })? | ||||
.contains(requirements::SHARESAFE_REQUIREMENT); | ||||
if share_safe && !source_is_share_safe { | ||||
Simon Sapin
|
r47215 | return Err(match config | ||
Simon Sapin
|
r47469 | .get(b"share", b"safe-mismatch.source-not-safe") | ||
Simon Sapin
|
r47215 | { | ||
Simon Sapin
|
r47214 | Some(b"abort") | None => HgError::abort( | ||
Simon Sapin
|
r47469 | "abort: share source does not support share-safe requirement\n\ | ||
(see `hg help config.format.use-share-safe` for more information)", | ||||
Pulkit Goyal
|
r48199 | exit_codes::ABORT, | ||
Simon Sapin
|
r47214 | ), | ||
Simon Sapin
|
r47215 | _ => HgError::unsupported("share-safe downgrade"), | ||
} | ||||
.into()); | ||||
Simon Sapin
|
r47191 | } else if source_is_share_safe && !share_safe { | ||
Simon Sapin
|
r47214 | return Err( | ||
Simon Sapin
|
r47469 | match config.get(b"share", b"safe-mismatch.source-safe") { | ||
Simon Sapin
|
r47214 | Some(b"abort") | None => HgError::abort( | ||
Simon Sapin
|
r47469 | "abort: version mismatch: source uses share-safe \ | ||
functionality while the current share does not\n\ | ||||
(see `hg help config.format.use-share-safe` for more information)", | ||||
Pulkit Goyal
|
r48199 | exit_codes::ABORT, | ||
Simon Sapin
|
r47214 | ), | ||
_ => HgError::unsupported("share-safe upgrade"), | ||||
Simon Sapin
|
r47215 | } | ||
.into(), | ||||
Simon Sapin
|
r47214 | ); | ||
Simon Sapin
|
r47191 | } | ||
Simon Sapin
|
r47215 | |||
if share_safe { | ||||
repo_config_files.insert(0, shared_path.join("hgrc")) | ||||
} | ||||
Simon Sapin
|
r47190 | } | ||
Simon Sapin
|
r47357 | if share_safe { | ||
reqs.extend(requirements::load(Vfs { base: &store_path })?); | ||||
} | ||||
Simon Sapin
|
r47190 | |||
Simon Sapin
|
r47475 | let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() { | ||
config.combine_with_repo(&repo_config_files)? | ||||
} else { | ||||
config.clone() | ||||
}; | ||||
Simon Sapin
|
r47215 | |||
Simon Sapin
|
r47190 | let repo = Self { | ||
requirements: reqs, | ||||
working_directory, | ||||
store: store_path, | ||||
dot_hg, | ||||
Simon Sapin
|
r47215 | config: repo_config, | ||
Simon Sapin
|
r47190 | }; | ||
requirements::check(&repo)?; | ||||
Ok(repo) | ||||
} | ||||
Simon Sapin
|
r46782 | pub fn working_directory_path(&self) -> &Path { | ||
&self.working_directory | ||||
} | ||||
Simon Sapin
|
r47190 | pub fn requirements(&self) -> &HashSet<String> { | ||
&self.requirements | ||||
} | ||||
Simon Sapin
|
r47215 | pub fn config(&self) -> &Config { | ||
&self.config | ||||
} | ||||
Simon Sapin
|
r46782 | /// For accessing repository files (in `.hg`), except for the store | ||
/// (`.hg/store`). | ||||
Simon Sapin
|
r47341 | pub fn hg_vfs(&self) -> Vfs<'_> { | ||
Simon Sapin
|
r46782 | Vfs { base: &self.dot_hg } | ||
} | ||||
/// For accessing repository store files (in `.hg/store`) | ||||
Simon Sapin
|
r47341 | pub fn store_vfs(&self) -> Vfs<'_> { | ||
Simon Sapin
|
r46782 | Vfs { base: &self.store } | ||
} | ||||
/// For accessing the working copy | ||||
Georges Racinet
|
r47578 | pub fn working_directory_vfs(&self) -> Vfs<'_> { | ||
Simon Sapin
|
r46782 | Vfs { | ||
base: &self.working_directory, | ||||
} | ||||
} | ||||
Simon Sapin
|
r47343 | |||
Simon Sapin
|
r48165 | pub fn has_dirstate_v2(&self) -> bool { | ||
self.requirements | ||||
.contains(requirements::DIRSTATE_V2_REQUIREMENT) | ||||
} | ||||
Simon Sapin
|
r47343 | pub fn dirstate_parents( | ||
&self, | ||||
) -> Result<crate::dirstate::DirstateParents, HgError> { | ||||
let dirstate = self.hg_vfs().mmap_open("dirstate")?; | ||||
Simon Sapin
|
r48165 | if dirstate.is_empty() { | ||
return Ok(crate::dirstate::DirstateParents::NULL); | ||||
} | ||||
let parents = if self.has_dirstate_v2() { | ||||
Simon Sapin
|
r48474 | crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents() | ||
Simon Sapin
|
r48165 | } else { | ||
crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? | ||||
Simon Sapin
|
r48474 | .clone() | ||
Simon Sapin
|
r48165 | }; | ||
Simon Sapin
|
r48474 | Ok(parents) | ||
Simon Sapin
|
r47343 | } | ||
Simon Sapin
|
r46782 | } | ||
impl Vfs<'_> { | ||||
Simon Sapin
|
r47341 | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | ||
Simon Sapin
|
r47175 | self.base.join(relative_path) | ||
} | ||||
Simon Sapin
|
r47341 | pub fn read( | ||
Simon Sapin
|
r46782 | &self, | ||
relative_path: impl AsRef<Path>, | ||||
Simon Sapin
|
r47172 | ) -> Result<Vec<u8>, HgError> { | ||
Simon Sapin
|
r47175 | let path = self.join(relative_path); | ||
Simon Sapin
|
r47341 | std::fs::read(&path).when_reading_file(&path) | ||
Simon Sapin
|
r46782 | } | ||
Simon Sapin
|
r47341 | pub fn mmap_open( | ||
Simon Sapin
|
r46782 | &self, | ||
relative_path: impl AsRef<Path>, | ||||
Simon Sapin
|
r47172 | ) -> Result<Mmap, HgError> { | ||
let path = self.base.join(relative_path); | ||||
Simon Sapin
|
r47341 | let file = std::fs::File::open(&path).when_reading_file(&path)?; | ||
Simon Sapin
|
r46782 | // TODO: what are the safety requirements here? | ||
Simon Sapin
|
r47341 | let mmap = unsafe { MmapOptions::new().map(&file) } | ||
.when_reading_file(&path)?; | ||||
Simon Sapin
|
r46782 | Ok(mmap) | ||
} | ||||
Simon Sapin
|
r47341 | |||
pub fn rename( | ||||
&self, | ||||
relative_from: impl AsRef<Path>, | ||||
relative_to: impl AsRef<Path>, | ||||
) -> Result<(), HgError> { | ||||
let from = self.join(relative_from); | ||||
let to = self.join(relative_to); | ||||
std::fs::rename(&from, &to) | ||||
.with_context(|| IoErrorContext::RenamingFile { from, to }) | ||||
} | ||||
Simon Sapin
|
r46782 | } | ||
Simon Sapin
|
r48584 | |||
fn fs_metadata( | ||||
path: impl AsRef<Path>, | ||||
) -> Result<Option<std::fs::Metadata>, HgError> { | ||||
let path = path.as_ref(); | ||||
match std::fs::metadata(path) { | ||||
Ok(meta) => Ok(Some(meta)), | ||||
Err(error) => match error.kind() { | ||||
// TODO: when we require a Rust version where `NotADirectory` is | ||||
// stable, invert this logic and return None for it and `NotFound` | ||||
// and propagate any other error. | ||||
ErrorKind::PermissionDenied => Err(error).with_context(|| { | ||||
IoErrorContext::ReadingMetadata(path.to_owned()) | ||||
}), | ||||
_ => Ok(None), | ||||
}, | ||||
} | ||||
} | ||||
fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> { | ||||
Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir())) | ||||
} | ||||
fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> { | ||||
Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file())) | ||||
} | ||||