repo.rs
339 lines
| 11.7 KiB
| application/rls-services+xml
|
RustLexer
|
r47215 | use crate::config::{Config, ConfigError, ConfigParseError}; | ||
|
r48768 | use crate::dirstate::DirstateParents; | ||
use crate::dirstate_tree::dirstate_map::DirstateMap; | ||||
use crate::dirstate_tree::owning::OwningDirstateMap; | ||||
|
r48764 | use crate::errors::HgError; | ||
|
r48768 | use crate::errors::HgResultExt; | ||
|
r48199 | use crate::exit_codes; | ||
|
r46782 | use crate::requirements; | ||
|
r47190 | use crate::utils::files::get_path_from_bytes; | ||
|
r47474 | use crate::utils::SliceExt; | ||
|
r48764 | use crate::vfs::{is_dir, is_file, Vfs}; | ||
|
r48768 | use crate::DirstateError; | ||
use std::cell::{Cell, Ref, RefCell, RefMut}; | ||||
|
r47190 | use std::collections::HashSet; | ||
|
r46782 | use std::path::{Path, PathBuf}; | ||
/// A repository on disk | ||||
pub struct Repo { | ||||
working_directory: PathBuf, | ||||
dot_hg: PathBuf, | ||||
store: PathBuf, | ||||
|
r47190 | requirements: HashSet<String>, | ||
|
r47215 | config: Config, | ||
|
r48768 | // None means not known/initialized yet | ||
dirstate_parents: Cell<Option<DirstateParents>>, | ||||
dirstate_map: RefCell<Option<OwningDirstateMap>>, | ||||
|
r46782 | } | ||
|
r47175 | #[derive(Debug, derive_more::From)] | ||
|
r47215 | pub enum RepoError { | ||
NotFound { | ||||
|
r47253 | at: PathBuf, | ||
|
r47175 | }, | ||
#[from] | ||||
|
r47215 | ConfigParseError(ConfigParseError), | ||
#[from] | ||||
|
r47175 | Other(HgError), | ||
} | ||||
|
r47215 | impl From<ConfigError> for RepoError { | ||
fn from(error: ConfigError) -> Self { | ||||
match error { | ||||
ConfigError::Parse(error) => error.into(), | ||||
ConfigError::Other(error) => error.into(), | ||||
} | ||||
} | ||||
} | ||||
|
r46782 | impl Repo { | ||
|
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() { | ||||
|
r48584 | if is_dir(ancestor.join(".hg"))? { | ||
|
r48197 | return Ok(ancestor.to_path_buf()); | ||
} | ||||
} | ||||
return Err(RepoError::NotFound { | ||||
at: current_directory, | ||||
}); | ||||
} | ||||
|
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. | ||||
|
r47253 | /// | ||
|
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. | ||||
|
r47253 | pub fn find( | ||
config: &Config, | ||||
|
r48196 | explicit_path: Option<PathBuf>, | ||
|
r47253 | ) -> Result<Self, RepoError> { | ||
if let Some(root) = explicit_path { | ||||
|
r48584 | if is_dir(root.join(".hg"))? { | ||
|
r47474 | Self::new_at_path(root.to_owned(), config) | ||
|
r48584 | } else if is_file(&root)? { | ||
|
r47464 | Err(HgError::unsupported("bundle repository").into()) | ||
|
r47253 | } else { | ||
Err(RepoError::NotFound { | ||||
at: root.to_owned(), | ||||
}) | ||||
|
r47175 | } | ||
|
r47253 | } else { | ||
|
r48197 | let root = Self::find_repo_root()?; | ||
Self::new_at_path(root, config) | ||||
|
r46782 | } | ||
} | ||||
|
r47190 | /// To be called after checking that `.hg` is a sub-directory | ||
|
r47214 | fn new_at_path( | ||
working_directory: PathBuf, | ||||
config: &Config, | ||||
|
r47215 | ) -> Result<Self, RepoError> { | ||
|
r47190 | let dot_hg = working_directory.join(".hg"); | ||
|
r47191 | |||
|
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")); | ||||
|
r47190 | let hg_vfs = Vfs { base: &dot_hg }; | ||
|
r47191 | let mut reqs = requirements::load_if_exists(hg_vfs)?; | ||
|
r47190 | let relative = | ||
reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); | ||||
let shared = | ||||
reqs.contains(requirements::SHARED_REQUIREMENT) || relative; | ||||
|
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); | ||||
|
r47190 | let store_path; | ||
if !shared { | ||||
store_path = dot_hg.join("store"); | ||||
} else { | ||||
let bytes = hg_vfs.read("sharedpath")?; | ||||
|
r47427 | let mut shared_path = | ||
|
r48761 | get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n')) | ||
.to_owned(); | ||||
|
r47190 | if relative { | ||
shared_path = dot_hg.join(shared_path) | ||||
} | ||||
|
r48584 | if !is_dir(&shared_path)? { | ||
|
r47190 | return Err(HgError::corrupted(format!( | ||
".hg/sharedpath points to nonexistent directory {}", | ||||
shared_path.display() | ||||
|
r47215 | )) | ||
.into()); | ||||
|
r47190 | } | ||
store_path = shared_path.join("store"); | ||||
|
r47191 | |||
let source_is_share_safe = | ||||
requirements::load(Vfs { base: &shared_path })? | ||||
.contains(requirements::SHARESAFE_REQUIREMENT); | ||||
if share_safe && !source_is_share_safe { | ||||
|
r47215 | return Err(match config | ||
|
r47469 | .get(b"share", b"safe-mismatch.source-not-safe") | ||
|
r47215 | { | ||
|
r47214 | Some(b"abort") | None => HgError::abort( | ||
|
r47469 | "abort: share source does not support share-safe requirement\n\ | ||
(see `hg help config.format.use-share-safe` for more information)", | ||||
|
r48199 | exit_codes::ABORT, | ||
|
r47214 | ), | ||
|
r47215 | _ => HgError::unsupported("share-safe downgrade"), | ||
} | ||||
.into()); | ||||
|
r47191 | } else if source_is_share_safe && !share_safe { | ||
|
r47214 | return Err( | ||
|
r47469 | match config.get(b"share", b"safe-mismatch.source-safe") { | ||
|
r47214 | Some(b"abort") | None => HgError::abort( | ||
|
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)", | ||||
|
r48199 | exit_codes::ABORT, | ||
|
r47214 | ), | ||
_ => HgError::unsupported("share-safe upgrade"), | ||||
|
r47215 | } | ||
.into(), | ||||
|
r47214 | ); | ||
|
r47191 | } | ||
|
r47215 | |||
if share_safe { | ||||
repo_config_files.insert(0, shared_path.join("hgrc")) | ||||
} | ||||
|
r47190 | } | ||
|
r47357 | if share_safe { | ||
reqs.extend(requirements::load(Vfs { base: &store_path })?); | ||||
} | ||||
|
r47190 | |||
|
r47475 | let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() { | ||
config.combine_with_repo(&repo_config_files)? | ||||
} else { | ||||
config.clone() | ||||
}; | ||||
|
r47215 | |||
|
r47190 | let repo = Self { | ||
requirements: reqs, | ||||
working_directory, | ||||
store: store_path, | ||||
dot_hg, | ||||
|
r47215 | config: repo_config, | ||
|
r48768 | dirstate_parents: Cell::new(None), | ||
dirstate_map: RefCell::new(None), | ||||
|
r47190 | }; | ||
requirements::check(&repo)?; | ||||
Ok(repo) | ||||
} | ||||
|
r46782 | pub fn working_directory_path(&self) -> &Path { | ||
&self.working_directory | ||||
} | ||||
|
r47190 | pub fn requirements(&self) -> &HashSet<String> { | ||
&self.requirements | ||||
} | ||||
|
r47215 | pub fn config(&self) -> &Config { | ||
&self.config | ||||
} | ||||
|
r46782 | /// For accessing repository files (in `.hg`), except for the store | ||
/// (`.hg/store`). | ||||
|
r47341 | pub fn hg_vfs(&self) -> Vfs<'_> { | ||
|
r46782 | Vfs { base: &self.dot_hg } | ||
} | ||||
/// For accessing repository store files (in `.hg/store`) | ||||
|
r47341 | pub fn store_vfs(&self) -> Vfs<'_> { | ||
|
r46782 | Vfs { base: &self.store } | ||
} | ||||
/// For accessing the working copy | ||||
|
r47578 | pub fn working_directory_vfs(&self) -> Vfs<'_> { | ||
|
r46782 | Vfs { | ||
base: &self.working_directory, | ||||
} | ||||
} | ||||
|
r47343 | |||
|
r48165 | pub fn has_dirstate_v2(&self) -> bool { | ||
self.requirements | ||||
.contains(requirements::DIRSTATE_V2_REQUIREMENT) | ||||
} | ||||
|
r48768 | fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> { | ||
Ok(self | ||||
.hg_vfs() | ||||
.read("dirstate") | ||||
.io_not_found_as_none()? | ||||
.unwrap_or(Vec::new())) | ||||
} | ||||
pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> { | ||||
if let Some(parents) = self.dirstate_parents.get() { | ||||
return Ok(parents); | ||||
|
r48165 | } | ||
|
r48768 | let dirstate = self.dirstate_file_contents()?; | ||
let parents = if dirstate.is_empty() { | ||||
DirstateParents::NULL | ||||
} else if self.has_dirstate_v2() { | ||||
|
r48474 | crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents() | ||
|
r48165 | } else { | ||
crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? | ||||
|
r48474 | .clone() | ||
|
r48165 | }; | ||
|
r48768 | self.dirstate_parents.set(Some(parents)); | ||
|
r48474 | Ok(parents) | ||
|
r47343 | } | ||
|
r48768 | |||
fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> { | ||||
let dirstate_file_contents = self.dirstate_file_contents()?; | ||||
if dirstate_file_contents.is_empty() { | ||||
self.dirstate_parents.set(Some(DirstateParents::NULL)); | ||||
Ok(OwningDirstateMap::new_empty(Vec::new())) | ||||
} else if self.has_dirstate_v2() { | ||||
let docket = crate::dirstate_tree::on_disk::read_docket( | ||||
&dirstate_file_contents, | ||||
)?; | ||||
self.dirstate_parents.set(Some(docket.parents())); | ||||
let data_size = docket.data_size(); | ||||
let metadata = docket.tree_metadata(); | ||||
let mut map = if let Some(data_mmap) = self | ||||
.hg_vfs() | ||||
.mmap_open(docket.data_filename()) | ||||
.io_not_found_as_none()? | ||||
{ | ||||
OwningDirstateMap::new_empty(MmapWrapper(data_mmap)) | ||||
} else { | ||||
OwningDirstateMap::new_empty(Vec::new()) | ||||
}; | ||||
let (on_disk, placeholder) = map.get_mut_pair(); | ||||
*placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?; | ||||
Ok(map) | ||||
} else { | ||||
let mut map = OwningDirstateMap::new_empty(dirstate_file_contents); | ||||
let (on_disk, placeholder) = map.get_mut_pair(); | ||||
let (inner, parents) = DirstateMap::new_v1(on_disk)?; | ||||
self.dirstate_parents | ||||
.set(Some(parents.unwrap_or(DirstateParents::NULL))); | ||||
*placeholder = inner; | ||||
Ok(map) | ||||
} | ||||
} | ||||
pub fn dirstate_map( | ||||
&self, | ||||
) -> Result<Ref<OwningDirstateMap>, DirstateError> { | ||||
let mut borrowed = self.dirstate_map.borrow(); | ||||
if borrowed.is_none() { | ||||
drop(borrowed); | ||||
// Only use `borrow_mut` if it is really needed to avoid panic in | ||||
// case there is another outstanding borrow but mutation is not | ||||
// needed. | ||||
*self.dirstate_map.borrow_mut() = Some(self.new_dirstate_map()?); | ||||
borrowed = self.dirstate_map.borrow() | ||||
} | ||||
Ok(Ref::map(borrowed, |option| option.as_ref().unwrap())) | ||||
} | ||||
pub fn dirstate_map_mut( | ||||
&self, | ||||
) -> Result<RefMut<OwningDirstateMap>, DirstateError> { | ||||
let mut borrowed = self.dirstate_map.borrow_mut(); | ||||
if borrowed.is_none() { | ||||
*borrowed = Some(self.new_dirstate_map()?); | ||||
} | ||||
Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap())) | ||||
} | ||||
|
r46782 | } | ||
|
r48768 | |||
// TODO: remove this when https://github.com/RazrFalcon/memmap2-rs/pull/22 is on crates.io | ||||
struct MmapWrapper(memmap2::Mmap); | ||||
impl std::ops::Deref for MmapWrapper { | ||||
type Target = [u8]; | ||||
fn deref(&self) -> &[u8] { | ||||
self.0.deref() | ||||
} | ||||
} | ||||
unsafe impl stable_deref_trait::StableDeref for MmapWrapper {} | ||||