repo.rs
523 lines
| 17.7 KiB
| application/rls-services+xml
|
RustLexer
|
r48773 | use crate::changelog::Changelog; | ||
|
r47215 | use crate::config::{Config, ConfigError, ConfigParseError}; | ||
|
r48768 | use crate::dirstate::DirstateParents; | ||
|
r49249 | use crate::dirstate_tree::on_disk::Docket as DirstateDocket; | ||
|
r48768 | use crate::dirstate_tree::owning::OwningDirstateMap; | ||
use crate::errors::HgResultExt; | ||||
|
r49249 | use crate::errors::{HgError, IoResultExt}; | ||
|
r49245 | use crate::lock::{try_with_lock_no_wait, LockError}; | ||
|
r48774 | use crate::manifest::{Manifest, Manifestlog}; | ||
|
r48775 | use crate::revlog::filelog::Filelog; | ||
|
r48773 | use crate::revlog::revlog::RevlogError; | ||
|
r47190 | use crate::utils::files::get_path_from_bytes; | ||
|
r48775 | use crate::utils::hg_path::HgPath; | ||
|
r47474 | use crate::utils::SliceExt; | ||
|
r48764 | use crate::vfs::{is_dir, is_file, Vfs}; | ||
|
r48778 | use crate::{requirements, NodePrefix}; | ||
|
r48774 | use crate::{DirstateError, Revision}; | ||
|
r49247 | use std::cell::{Ref, RefCell, RefMut}; | ||
|
r47190 | use std::collections::HashSet; | ||
|
r49249 | use std::io::Seek; | ||
use std::io::SeekFrom; | ||||
use std::io::Write as IoWrite; | ||||
|
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, | ||
|
r49247 | dirstate_parents: LazyCell<DirstateParents, HgError>, | ||
|
r49248 | dirstate_data_file_uuid: LazyCell<Option<Vec<u8>>, HgError>, | ||
|
r48772 | dirstate_map: LazyCell<OwningDirstateMap, DirstateError>, | ||
|
r48777 | changelog: LazyCell<Changelog, HgError>, | ||
manifestlog: LazyCell<Manifestlog, HgError>, | ||||
|
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); | ||||
|
r49664 | if share_safe != source_is_share_safe { | ||
return Err(HgError::unsupported("share-safe mismatch").into()); | ||||
|
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, | ||
|
r49247 | dirstate_parents: LazyCell::new(Self::read_dirstate_parents), | ||
|
r49248 | dirstate_data_file_uuid: LazyCell::new( | ||
Self::read_dirstate_data_file_uuid, | ||||
), | ||||
|
r48772 | dirstate_map: LazyCell::new(Self::new_dirstate_map), | ||
|
r49981 | changelog: LazyCell::new(Self::new_changelog), | ||
manifestlog: LazyCell::new(Self::new_manifestlog), | ||||
|
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 | |||
|
r49245 | pub fn try_with_wlock_no_wait<R>( | ||
&self, | ||||
f: impl FnOnce() -> R, | ||||
) -> Result<R, LockError> { | ||||
try_with_lock_no_wait(self.hg_vfs(), "wlock", f) | ||||
} | ||||
|
r48165 | pub fn has_dirstate_v2(&self) -> bool { | ||
self.requirements | ||||
.contains(requirements::DIRSTATE_V2_REQUIREMENT) | ||||
} | ||||
|
r49238 | pub fn has_sparse(&self) -> bool { | ||
self.requirements.contains(requirements::SPARSE_REQUIREMENT) | ||||
} | ||||
pub fn has_narrow(&self) -> bool { | ||||
self.requirements.contains(requirements::NARROW_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> { | ||||
|
r49247 | Ok(*self.dirstate_parents.get_or_init(self)?) | ||
} | ||||
fn read_dirstate_parents(&self) -> Result<DirstateParents, HgError> { | ||||
|
r48768 | let dirstate = self.dirstate_file_contents()?; | ||
let parents = if dirstate.is_empty() { | ||||
|
r49248 | if self.has_dirstate_v2() { | ||
self.dirstate_data_file_uuid.set(None); | ||||
} | ||||
|
r48768 | DirstateParents::NULL | ||
} else if self.has_dirstate_v2() { | ||||
|
r49248 | let docket = | ||
crate::dirstate_tree::on_disk::read_docket(&dirstate)?; | ||||
self.dirstate_data_file_uuid | ||||
.set(Some(docket.uuid.to_owned())); | ||||
docket.parents() | ||||
|
r48165 | } else { | ||
crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? | ||||
|
r48474 | .clone() | ||
|
r48165 | }; | ||
|
r49247 | self.dirstate_parents.set(parents); | ||
|
r48474 | Ok(parents) | ||
|
r47343 | } | ||
|
r48768 | |||
|
r49248 | fn read_dirstate_data_file_uuid( | ||
&self, | ||||
) -> Result<Option<Vec<u8>>, HgError> { | ||||
assert!( | ||||
self.has_dirstate_v2(), | ||||
"accessing dirstate data file ID without dirstate-v2" | ||||
); | ||||
let dirstate = self.dirstate_file_contents()?; | ||||
if dirstate.is_empty() { | ||||
self.dirstate_parents.set(DirstateParents::NULL); | ||||
Ok(None) | ||||
} else { | ||||
let docket = | ||||
crate::dirstate_tree::on_disk::read_docket(&dirstate)?; | ||||
self.dirstate_parents.set(docket.parents()); | ||||
Ok(Some(docket.uuid.to_owned())) | ||||
} | ||||
} | ||||
|
r48768 | fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> { | ||
let dirstate_file_contents = self.dirstate_file_contents()?; | ||||
if dirstate_file_contents.is_empty() { | ||||
|
r49247 | self.dirstate_parents.set(DirstateParents::NULL); | ||
|
r49248 | if self.has_dirstate_v2() { | ||
self.dirstate_data_file_uuid.set(None); | ||||
} | ||||
|
r48768 | Ok(OwningDirstateMap::new_empty(Vec::new())) | ||
} else if self.has_dirstate_v2() { | ||||
let docket = crate::dirstate_tree::on_disk::read_docket( | ||||
&dirstate_file_contents, | ||||
)?; | ||||
|
r49247 | self.dirstate_parents.set(docket.parents()); | ||
|
r49248 | self.dirstate_data_file_uuid | ||
.set(Some(docket.uuid.to_owned())); | ||||
|
r48768 | let data_size = docket.data_size(); | ||
let metadata = docket.tree_metadata(); | ||||
|
r49864 | if let Some(data_mmap) = self | ||
|
r48768 | .hg_vfs() | ||
.mmap_open(docket.data_filename()) | ||||
.io_not_found_as_none()? | ||||
{ | ||||
|
r49864 | OwningDirstateMap::new_v2(data_mmap, data_size, metadata) | ||
|
r48768 | } else { | ||
|
r49864 | OwningDirstateMap::new_v2(Vec::new(), data_size, metadata) | ||
} | ||||
|
r48768 | } else { | ||
|
r49864 | let (map, parents) = | ||
OwningDirstateMap::new_v1(dirstate_file_contents)?; | ||||
self.dirstate_parents.set(parents); | ||||
|
r48768 | Ok(map) | ||
} | ||||
} | ||||
pub fn dirstate_map( | ||||
&self, | ||||
) -> Result<Ref<OwningDirstateMap>, DirstateError> { | ||||
|
r48772 | self.dirstate_map.get_or_init(self) | ||
} | ||||
pub fn dirstate_map_mut( | ||||
&self, | ||||
) -> Result<RefMut<OwningDirstateMap>, DirstateError> { | ||||
self.dirstate_map.get_mut_or_init(self) | ||||
} | ||||
|
r48773 | |||
|
r49981 | fn new_changelog(&self) -> Result<Changelog, HgError> { | ||
let use_nodemap = self | ||||
.requirements | ||||
.contains(requirements::NODEMAP_REQUIREMENT); | ||||
Changelog::open(&self.store_vfs(), use_nodemap) | ||||
} | ||||
|
r48777 | pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> { | ||
|
r48773 | self.changelog.get_or_init(self) | ||
} | ||||
|
r48777 | pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> { | ||
|
r48773 | self.changelog.get_mut_or_init(self) | ||
} | ||||
|
r49981 | fn new_manifestlog(&self) -> Result<Manifestlog, HgError> { | ||
let use_nodemap = self | ||||
.requirements | ||||
.contains(requirements::NODEMAP_REQUIREMENT); | ||||
Manifestlog::open(&self.store_vfs(), use_nodemap) | ||||
} | ||||
|
r48777 | pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> { | ||
|
r48773 | self.manifestlog.get_or_init(self) | ||
} | ||||
|
r48777 | pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> { | ||
|
r48773 | self.manifestlog.get_mut_or_init(self) | ||
} | ||||
|
r48774 | |||
|
r48783 | /// Returns the manifest of the *changeset* with the given node ID | ||
|
r48778 | pub fn manifest_for_node( | ||
&self, | ||||
node: impl Into<NodePrefix>, | ||||
) -> Result<Manifest, RevlogError> { | ||||
|
r48783 | self.manifestlog()?.data_for_node( | ||
|
r48778 | self.changelog()? | ||
|
r48783 | .data_for_node(node.into())? | ||
|
r48778 | .manifest_node()? | ||
.into(), | ||||
) | ||||
} | ||||
|
r48783 | /// Returns the manifest of the *changeset* with the given revision number | ||
|
r48778 | pub fn manifest_for_rev( | ||
|
r48774 | &self, | ||
revision: Revision, | ||||
) -> Result<Manifest, RevlogError> { | ||||
|
r48783 | self.manifestlog()?.data_for_node( | ||
self.changelog()? | ||||
.data_for_rev(revision)? | ||||
.manifest_node()? | ||||
.into(), | ||||
|
r48778 | ) | ||
|
r48774 | } | ||
|
r48775 | |||
|
r49341 | pub fn has_subrepos(&self) -> Result<bool, DirstateError> { | ||
if let Some(entry) = self.dirstate_map()?.get(HgPath::new(".hgsub"))? { | ||||
Ok(entry.state().is_tracked()) | ||||
} else { | ||||
Ok(false) | ||||
} | ||||
} | ||||
|
r48777 | pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> { | ||
|
r48775 | Filelog::open(self, path) | ||
} | ||||
|
r49249 | |||
/// Write to disk any updates that were made through `dirstate_map_mut`. | ||||
/// | ||||
/// The "wlock" must be held while calling this. | ||||
/// See for example `try_with_wlock_no_wait`. | ||||
/// | ||||
/// TODO: have a `WritableRepo` type only accessible while holding the | ||||
/// lock? | ||||
pub fn write_dirstate(&self) -> Result<(), DirstateError> { | ||||
let map = self.dirstate_map()?; | ||||
// TODO: Maintain a `DirstateMap::dirty` flag, and return early here if | ||||
// it’s unset | ||||
let parents = self.dirstate_parents()?; | ||||
let packed_dirstate = if self.has_dirstate_v2() { | ||||
let uuid = self.dirstate_data_file_uuid.get_or_init(self)?; | ||||
let mut uuid = uuid.as_ref(); | ||||
let can_append = uuid.is_some(); | ||||
let (data, tree_metadata, append) = map.pack_v2(can_append)?; | ||||
if !append { | ||||
uuid = None | ||||
} | ||||
let uuid = if let Some(uuid) = uuid { | ||||
std::str::from_utf8(uuid) | ||||
.map_err(|_| { | ||||
HgError::corrupted("non-UTF-8 dirstate data file ID") | ||||
})? | ||||
.to_owned() | ||||
} else { | ||||
DirstateDocket::new_uid() | ||||
}; | ||||
let data_filename = format!("dirstate.{}", uuid); | ||||
let data_filename = self.hg_vfs().join(data_filename); | ||||
let mut options = std::fs::OpenOptions::new(); | ||||
if append { | ||||
options.append(true); | ||||
} else { | ||||
options.write(true).create_new(true); | ||||
} | ||||
let data_size = (|| { | ||||
// TODO: loop and try another random ID if !append and this | ||||
// returns `ErrorKind::AlreadyExists`? Collision chance of two | ||||
// random IDs is one in 2**32 | ||||
let mut file = options.open(&data_filename)?; | ||||
file.write_all(&data)?; | ||||
file.flush()?; | ||||
// TODO: use https://doc.rust-lang.org/std/io/trait.Seek.html#method.stream_position when we require Rust 1.51+ | ||||
file.seek(SeekFrom::Current(0)) | ||||
})() | ||||
.when_writing_file(&data_filename)?; | ||||
DirstateDocket::serialize( | ||||
parents, | ||||
tree_metadata, | ||||
data_size, | ||||
uuid.as_bytes(), | ||||
) | ||||
.map_err(|_: std::num::TryFromIntError| { | ||||
HgError::corrupted("overflow in dirstate docket serialization") | ||||
})? | ||||
} else { | ||||
map.pack_v1(parents)? | ||||
}; | ||||
self.hg_vfs().atomic_write("dirstate", &packed_dirstate)?; | ||||
Ok(()) | ||||
} | ||||
|
r48772 | } | ||
/// Lazily-initialized component of `Repo` with interior mutability | ||||
/// | ||||
/// This differs from `OnceCell` in that the value can still be "deinitialized" | ||||
/// later by setting its inner `Option` to `None`. | ||||
struct LazyCell<T, E> { | ||||
value: RefCell<Option<T>>, | ||||
// `Fn`s that don’t capture environment are zero-size, so this box does | ||||
// not allocate: | ||||
init: Box<dyn Fn(&Repo) -> Result<T, E>>, | ||||
} | ||||
impl<T, E> LazyCell<T, E> { | ||||
fn new(init: impl Fn(&Repo) -> Result<T, E> + 'static) -> Self { | ||||
Self { | ||||
value: RefCell::new(None), | ||||
init: Box::new(init), | ||||
} | ||||
} | ||||
|
r49247 | fn set(&self, value: T) { | ||
*self.value.borrow_mut() = Some(value) | ||||
} | ||||
|
r48772 | fn get_or_init(&self, repo: &Repo) -> Result<Ref<T>, E> { | ||
let mut borrowed = self.value.borrow(); | ||||
|
r48768 | 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. | ||||
|
r48772 | *self.value.borrow_mut() = Some((self.init)(repo)?); | ||
borrowed = self.value.borrow() | ||||
|
r48768 | } | ||
Ok(Ref::map(borrowed, |option| option.as_ref().unwrap())) | ||||
} | ||||
|
r49247 | fn get_mut_or_init(&self, repo: &Repo) -> Result<RefMut<T>, E> { | ||
|
r48772 | let mut borrowed = self.value.borrow_mut(); | ||
|
r48768 | if borrowed.is_none() { | ||
|
r48772 | *borrowed = Some((self.init)(repo)?); | ||
|
r48768 | } | ||
Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap())) | ||||
} | ||||
|
r46782 | } | ||