repo.rs
855 lines
| 30.5 KiB
| application/rls-services+xml
|
RustLexer
Simon Sapin
|
r48773 | use crate::changelog::Changelog; | ||
Simon Sapin
|
r47215 | use crate::config::{Config, ConfigError, ConfigParseError}; | ||
Simon Sapin
|
r48768 | use crate::dirstate::DirstateParents; | ||
r51116 | use crate::dirstate_tree::dirstate_map::DirstateMapWriteMode; | |||
Simon Sapin
|
r49249 | use crate::dirstate_tree::on_disk::Docket as DirstateDocket; | ||
Simon Sapin
|
r48768 | use crate::dirstate_tree::owning::OwningDirstateMap; | ||
use crate::errors::HgResultExt; | ||||
Simon Sapin
|
r49249 | use crate::errors::{HgError, IoResultExt}; | ||
Simon Sapin
|
r49245 | use crate::lock::{try_with_lock_no_wait, LockError}; | ||
Simon Sapin
|
r48774 | use crate::manifest::{Manifest, Manifestlog}; | ||
Raphaël Gomès
|
r52084 | use crate::requirements::{ | ||
CHANGELOGV2_REQUIREMENT, GENERALDELTA_REQUIREMENT, NODEMAP_REQUIREMENT, | ||||
REVLOGV1_REQUIREMENT, REVLOGV2_REQUIREMENT, | ||||
}; | ||||
Simon Sapin
|
r48775 | use crate::revlog::filelog::Filelog; | ||
Raphaël Gomès
|
r50832 | use crate::revlog::RevlogError; | ||
r51123 | use crate::utils::debug::debug_wait_for_file_or_print; | |||
Simon Sapin
|
r47190 | use crate::utils::files::get_path_from_bytes; | ||
Simon Sapin
|
r48775 | use crate::utils::hg_path::HgPath; | ||
Simon Sapin
|
r47474 | use crate::utils::SliceExt; | ||
Raphaël Gomès
|
r52761 | use crate::vfs::{is_dir, is_file, VfsImpl}; | ||
Raphaël Gomès
|
r52084 | use crate::{ | ||
Raphaël Gomès
|
r52760 | requirements, NodePrefix, RevlogDataConfig, RevlogDeltaConfig, | ||
RevlogFeatureConfig, RevlogType, RevlogVersionOptions, UncheckedRevision, | ||||
Raphaël Gomès
|
r52084 | }; | ||
use crate::{DirstateError, RevlogOpenOptions}; | ||||
Simon Sapin
|
r49247 | use std::cell::{Ref, RefCell, RefMut}; | ||
Simon Sapin
|
r47190 | use std::collections::HashSet; | ||
Simon Sapin
|
r49249 | use std::io::Seek; | ||
use std::io::SeekFrom; | ||||
use std::io::Write as IoWrite; | ||||
Simon Sapin
|
r46782 | use std::path::{Path, PathBuf}; | ||
r51134 | const V2_MAX_READ_ATTEMPTS: usize = 5; | |||
r51147 | type DirstateMapIdentity = (Option<u64>, Option<Vec<u8>>, usize); | |||
Simon Sapin
|
r46782 | /// 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, | ||
Martin von Zweigbergk
|
r50072 | dirstate_parents: LazyCell<DirstateParents>, | ||
dirstate_map: LazyCell<OwningDirstateMap>, | ||||
changelog: LazyCell<Changelog>, | ||||
manifestlog: LazyCell<Manifestlog>, | ||||
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 | 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()); | ||
} | ||||
} | ||||
Raphaël Gomès
|
r50825 | Err(RepoError::NotFound { | ||
Pulkit Goyal
|
r48197 | at: current_directory, | ||
Raphaël Gomès
|
r50825 | }) | ||
Pulkit Goyal
|
r48197 | } | ||
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"))? { | ||
Raphaël Gomès
|
r50825 | Self::new_at_path(root, config) | ||
Simon Sapin
|
r48584 | } else if is_file(&root)? { | ||
Simon Sapin
|
r47464 | Err(HgError::unsupported("bundle repository").into()) | ||
Simon Sapin
|
r47253 | } else { | ||
Raphaël Gomès
|
r50825 | Err(RepoError::NotFound { at: root }) | ||
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 | |||
Raphaël Gomès
|
r50825 | let mut repo_config_files = | ||
vec![dot_hg.join("hgrc"), dot_hg.join("hgrc-not-shared")]; | ||||
Simon Sapin
|
r47215 | |||
Raphaël Gomès
|
r52761 | let hg_vfs = VfsImpl { | ||
base: dot_hg.to_owned(), | ||||
}; | ||||
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 = | ||
Simon Sapin
|
r48761 | get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n')) | ||
.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 | |||
Raphaël Gomès
|
r52761 | let source_is_share_safe = requirements::load(VfsImpl { | ||
base: shared_path.to_owned(), | ||||
})? | ||||
.contains(requirements::SHARESAFE_REQUIREMENT); | ||||
Simon Sapin
|
r47191 | |||
Arseniy Alekseyev
|
r49664 | if share_safe != source_is_share_safe { | ||
return Err(HgError::unsupported("share-safe mismatch").into()); | ||||
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 { | ||
Raphaël Gomès
|
r52761 | reqs.extend(requirements::load(VfsImpl { | ||
base: store_path.to_owned(), | ||||
})?); | ||||
Simon Sapin
|
r47357 | } | ||
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, | ||
Martin von Zweigbergk
|
r50072 | dirstate_parents: LazyCell::new(), | ||
dirstate_map: LazyCell::new(), | ||||
changelog: LazyCell::new(), | ||||
manifestlog: LazyCell::new(), | ||||
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`). | ||||
Raphaël Gomès
|
r52761 | pub fn hg_vfs(&self) -> VfsImpl { | ||
VfsImpl { | ||||
base: self.dot_hg.to_owned(), | ||||
} | ||||
Simon Sapin
|
r46782 | } | ||
/// For accessing repository store files (in `.hg/store`) | ||||
Raphaël Gomès
|
r52761 | pub fn store_vfs(&self) -> VfsImpl { | ||
VfsImpl { | ||||
base: self.store.to_owned(), | ||||
} | ||||
Simon Sapin
|
r46782 | } | ||
/// For accessing the working copy | ||||
Raphaël Gomès
|
r52761 | pub fn working_directory_vfs(&self) -> VfsImpl { | ||
VfsImpl { | ||||
base: self.working_directory.to_owned(), | ||||
Simon Sapin
|
r46782 | } | ||
} | ||||
Simon Sapin
|
r47343 | |||
Simon Sapin
|
r49245 | pub fn try_with_wlock_no_wait<R>( | ||
&self, | ||||
f: impl FnOnce() -> R, | ||||
) -> Result<R, LockError> { | ||||
Raphaël Gomès
|
r52761 | try_with_lock_no_wait(&self.hg_vfs(), "wlock", f) | ||
Simon Sapin
|
r49245 | } | ||
Raphaël Gomès
|
r51551 | /// Whether this repo should use dirstate-v2. | ||
/// The presence of `dirstate-v2` in the requirements does not mean that | ||||
/// the on-disk dirstate is necessarily in version 2. In most cases, | ||||
/// a dirstate-v2 file will indeed be found, but in rare cases (like the | ||||
/// upgrade mechanism being cut short), the on-disk version will be a | ||||
/// v1 file. | ||||
Raphaël Gomès
|
r51553 | /// Semantically, having a requirement only means that a client cannot | ||
/// properly understand or properly update the repo if it lacks the support | ||||
/// for the required feature, but not that that feature is actually used | ||||
/// in all occasions. | ||||
Raphaël Gomès
|
r51551 | pub fn use_dirstate_v2(&self) -> bool { | ||
Simon Sapin
|
r48165 | self.requirements | ||
.contains(requirements::DIRSTATE_V2_REQUIREMENT) | ||||
} | ||||
Arseniy Alekseyev
|
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) | ||||
} | ||||
Martin von Zweigbergk
|
r49982 | pub fn has_nodemap(&self) -> bool { | ||
self.requirements | ||||
.contains(requirements::NODEMAP_REQUIREMENT) | ||||
} | ||||
Simon Sapin
|
r48768 | fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> { | ||
Ok(self | ||||
.hg_vfs() | ||||
.read("dirstate") | ||||
.io_not_found_as_none()? | ||||
Raphaël Gomès
|
r50825 | .unwrap_or_default()) | ||
Simon Sapin
|
r48768 | } | ||
Raphaël Gomès
|
r51140 | fn dirstate_identity(&self) -> Result<Option<u64>, HgError> { | ||
use std::os::unix::fs::MetadataExt; | ||||
Ok(self | ||||
.hg_vfs() | ||||
.symlink_metadata("dirstate") | ||||
.io_not_found_as_none()? | ||||
.map(|meta| meta.ino())) | ||||
} | ||||
Simon Sapin
|
r48768 | pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> { | ||
Martin von Zweigbergk
|
r50072 | Ok(*self | ||
.dirstate_parents | ||||
.get_or_init(|| self.read_dirstate_parents())?) | ||||
Simon Sapin
|
r49247 | } | ||
fn read_dirstate_parents(&self) -> Result<DirstateParents, HgError> { | ||||
Simon Sapin
|
r48768 | let dirstate = self.dirstate_file_contents()?; | ||
let parents = if dirstate.is_empty() { | ||||
DirstateParents::NULL | ||||
Raphaël Gomès
|
r51551 | } else if self.use_dirstate_v2() { | ||
Raphaël Gomès
|
r51553 | let docket_res = | ||
crate::dirstate_tree::on_disk::read_docket(&dirstate); | ||||
match docket_res { | ||||
Ok(docket) => docket.parents(), | ||||
Err(_) => { | ||||
log::info!( | ||||
"Parsing dirstate docket failed, \ | ||||
falling back to dirstate-v1" | ||||
); | ||||
*crate::dirstate::parsers::parse_dirstate_parents( | ||||
&dirstate, | ||||
)? | ||||
} | ||||
} | ||||
Simon Sapin
|
r48165 | } else { | ||
Raphaël Gomès
|
r50825 | *crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? | ||
Simon Sapin
|
r48165 | }; | ||
Simon Sapin
|
r49247 | self.dirstate_parents.set(parents); | ||
Simon Sapin
|
r48474 | Ok(parents) | ||
Simon Sapin
|
r47343 | } | ||
Simon Sapin
|
r48768 | |||
Raphaël Gomès
|
r51139 | /// Returns the information read from the dirstate docket necessary to | ||
/// check if the data file has been updated/deleted by another process | ||||
/// since we last read the dirstate. | ||||
Raphaël Gomès
|
r51140 | /// Namely, the inode, data file uuid and the data size. | ||
Raphaël Gomès
|
r51139 | fn get_dirstate_data_file_integrity( | ||
Simon Sapin
|
r49248 | &self, | ||
r51147 | ) -> Result<DirstateMapIdentity, HgError> { | |||
Simon Sapin
|
r49248 | assert!( | ||
Raphaël Gomès
|
r51551 | self.use_dirstate_v2(), | ||
Simon Sapin
|
r49248 | "accessing dirstate data file ID without dirstate-v2" | ||
); | ||||
Raphaël Gomès
|
r51140 | // Get the identity before the contents since we could have a race | ||
// between the two. Having an identity that is too old is fine, but | ||||
// one that is younger than the content change is bad. | ||||
let identity = self.dirstate_identity()?; | ||||
Simon Sapin
|
r49248 | let dirstate = self.dirstate_file_contents()?; | ||
if dirstate.is_empty() { | ||||
Raphaël Gomès
|
r51140 | Ok((identity, None, 0)) | ||
Simon Sapin
|
r49248 | } else { | ||
Raphaël Gomès
|
r51553 | let docket_res = | ||
crate::dirstate_tree::on_disk::read_docket(&dirstate); | ||||
match docket_res { | ||||
Ok(docket) => { | ||||
self.dirstate_parents.set(docket.parents()); | ||||
Ok(( | ||||
identity, | ||||
Some(docket.uuid.to_owned()), | ||||
docket.data_size(), | ||||
)) | ||||
} | ||||
Err(_) => { | ||||
log::info!( | ||||
"Parsing dirstate docket failed, \ | ||||
falling back to dirstate-v1" | ||||
); | ||||
let parents = | ||||
*crate::dirstate::parsers::parse_dirstate_parents( | ||||
&dirstate, | ||||
)?; | ||||
self.dirstate_parents.set(parents); | ||||
Ok((identity, None, 0)) | ||||
} | ||||
} | ||||
Simon Sapin
|
r49248 | } | ||
} | ||||
Simon Sapin
|
r48768 | fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> { | ||
Raphaël Gomès
|
r51551 | if self.use_dirstate_v2() { | ||
r51134 | // The v2 dirstate is split into a docket and a data file. | |||
// Since we don't always take the `wlock` to read it | ||||
// (like in `hg status`), it is susceptible to races. | ||||
// A simple retry method should be enough since full rewrites | ||||
// only happen when too much garbage data is present and | ||||
// this race is unlikely. | ||||
let mut tries = 0; | ||||
while tries < V2_MAX_READ_ATTEMPTS { | ||||
tries += 1; | ||||
match self.read_docket_and_data_file() { | ||||
Ok(m) => { | ||||
return Ok(m); | ||||
} | ||||
Err(e) => match e { | ||||
DirstateError::Common(HgError::RaceDetected( | ||||
context, | ||||
)) => { | ||||
log::info!( | ||||
"dirstate read race detected {} (retry {}/{})", | ||||
context, | ||||
tries, | ||||
V2_MAX_READ_ATTEMPTS, | ||||
); | ||||
continue; | ||||
} | ||||
Raphaël Gomès
|
r51553 | _ => { | ||
log::info!( | ||||
"Reading dirstate v2 failed, \ | ||||
falling back to v1" | ||||
); | ||||
return self.new_dirstate_map_v1(); | ||||
} | ||||
r51134 | }, | |||
} | ||||
} | ||||
let error = HgError::abort( | ||||
format!("dirstate read race happened {tries} times in a row"), | ||||
255, | ||||
None, | ||||
); | ||||
r51147 | Err(DirstateError::Common(error)) | |||
Raphaël Gomès
|
r51122 | } else { | ||
Raphaël Gomès
|
r51553 | self.new_dirstate_map_v1() | ||
} | ||||
} | ||||
fn new_dirstate_map_v1(&self) -> Result<OwningDirstateMap, DirstateError> { | ||||
debug_wait_for_file_or_print(self.config(), "dirstate.pre-read-file"); | ||||
let identity = self.dirstate_identity()?; | ||||
let dirstate_file_contents = self.dirstate_file_contents()?; | ||||
if dirstate_file_contents.is_empty() { | ||||
self.dirstate_parents.set(DirstateParents::NULL); | ||||
Raphaël Gomès
|
r52926 | Ok(OwningDirstateMap::new_empty(Vec::new(), identity)) | ||
Raphaël Gomès
|
r51553 | } else { | ||
let (map, parents) = | ||||
OwningDirstateMap::new_v1(dirstate_file_contents, identity)?; | ||||
self.dirstate_parents.set(parents); | ||||
Ok(map) | ||||
Raphaël Gomès
|
r51122 | } | ||
} | ||||
fn read_docket_and_data_file( | ||||
&self, | ||||
) -> Result<OwningDirstateMap, DirstateError> { | ||||
r51123 | debug_wait_for_file_or_print(self.config(), "dirstate.pre-read-file"); | |||
Simon Sapin
|
r48768 | let dirstate_file_contents = self.dirstate_file_contents()?; | ||
Raphaël Gomès
|
r51140 | let identity = self.dirstate_identity()?; | ||
Simon Sapin
|
r48768 | if dirstate_file_contents.is_empty() { | ||
Raphaël Gomès
|
r52926 | return Ok(OwningDirstateMap::new_empty(Vec::new(), identity)); | ||
Raphaël Gomès
|
r51122 | } | ||
let docket = crate::dirstate_tree::on_disk::read_docket( | ||||
&dirstate_file_contents, | ||||
)?; | ||||
Raphaël Gomès
|
r51129 | debug_wait_for_file_or_print( | ||
self.config(), | ||||
"dirstate.post-docket-read-file", | ||||
); | ||||
Raphaël Gomès
|
r51122 | self.dirstate_parents.set(docket.parents()); | ||
Raphaël Gomès
|
r51138 | let uuid = docket.uuid.to_owned(); | ||
Raphaël Gomès
|
r51122 | let data_size = docket.data_size(); | ||
r51134 | ||||
let context = "between reading dirstate docket and data file"; | ||||
let race_error = HgError::RaceDetected(context.into()); | ||||
Raphaël Gomès
|
r51122 | let metadata = docket.tree_metadata(); | ||
r51134 | ||||
Raphaël Gomès
|
r51122 | let mut map = if crate::vfs::is_on_nfs_mount(docket.data_filename()) { | ||
// Don't mmap on NFS to prevent `SIGBUS` error on deletion | ||||
r51134 | let contents = self.hg_vfs().read(docket.data_filename()); | |||
let contents = match contents { | ||||
Ok(c) => c, | ||||
Err(HgError::IoError { error, context }) => { | ||||
match error.raw_os_error().expect("real os error") { | ||||
// 2 = ENOENT, No such file or directory | ||||
// 116 = ESTALE, Stale NFS file handle | ||||
// | ||||
// TODO match on `error.kind()` when | ||||
// `ErrorKind::StaleNetworkFileHandle` is stable. | ||||
2 | 116 => { | ||||
// Race where the data file was deleted right after | ||||
// we read the docket, try again | ||||
return Err(race_error.into()); | ||||
} | ||||
_ => { | ||||
return Err( | ||||
HgError::IoError { error, context }.into() | ||||
) | ||||
} | ||||
} | ||||
} | ||||
Err(e) => return Err(e.into()), | ||||
}; | ||||
Raphaël Gomès
|
r51140 | OwningDirstateMap::new_v2( | ||
contents, data_size, metadata, uuid, identity, | ||||
) | ||||
Raphaël Gomès
|
r51122 | } else { | ||
r51134 | match self | |||
.hg_vfs() | ||||
.mmap_open(docket.data_filename()) | ||||
.io_not_found_as_none() | ||||
{ | ||||
Raphaël Gomès
|
r51138 | Ok(Some(data_mmap)) => OwningDirstateMap::new_v2( | ||
Raphaël Gomès
|
r51140 | data_mmap, data_size, metadata, uuid, identity, | ||
Raphaël Gomès
|
r51138 | ), | ||
r51134 | Ok(None) => { | |||
// Race where the data file was deleted right after we | ||||
// read the docket, try again | ||||
return Err(race_error.into()); | ||||
} | ||||
Err(e) => return Err(e.into()), | ||||
} | ||||
Raphaël Gomès
|
r51122 | }?; | ||
Raphaël Gomès
|
r51117 | |||
Raphaël Gomès
|
r51122 | let write_mode_config = self | ||
.config() | ||||
.get_str(b"devel", b"dirstate.v2.data_update_mode") | ||||
.unwrap_or(Some("auto")) | ||||
.unwrap_or("auto"); // don't bother for devel options | ||||
let write_mode = match write_mode_config { | ||||
"auto" => DirstateMapWriteMode::Auto, | ||||
"force-new" => DirstateMapWriteMode::ForceNewDataFile, | ||||
"force-append" => DirstateMapWriteMode::ForceAppend, | ||||
_ => DirstateMapWriteMode::Auto, | ||||
}; | ||||
Raphaël Gomès
|
r51117 | |||
Raphaël Gomès
|
r51122 | map.with_dmap_mut(|m| m.set_write_mode(write_mode)); | ||
Raphaël Gomès
|
r51117 | |||
Raphaël Gomès
|
r51122 | Ok(map) | ||
Simon Sapin
|
r48768 | } | ||
pub fn dirstate_map( | ||||
&self, | ||||
) -> Result<Ref<OwningDirstateMap>, DirstateError> { | ||||
Martin von Zweigbergk
|
r50072 | self.dirstate_map.get_or_init(|| self.new_dirstate_map()) | ||
Simon Sapin
|
r48772 | } | ||
pub fn dirstate_map_mut( | ||||
&self, | ||||
) -> Result<RefMut<OwningDirstateMap>, DirstateError> { | ||||
Martin von Zweigbergk
|
r50072 | self.dirstate_map | ||
.get_mut_or_init(|| self.new_dirstate_map()) | ||||
Simon Sapin
|
r48772 | } | ||
Simon Sapin
|
r48773 | |||
Martin von Zweigbergk
|
r49981 | fn new_changelog(&self) -> Result<Changelog, HgError> { | ||
Raphaël Gomès
|
r52760 | Changelog::open( | ||
&self.store_vfs(), | ||||
self.default_revlog_options(RevlogType::Changelog)?, | ||||
) | ||||
Simon Sapin
|
r48772 | } | ||
Simon Sapin
|
r48773 | |||
Simon Sapin
|
r48777 | pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> { | ||
Martin von Zweigbergk
|
r50072 | self.changelog.get_or_init(|| self.new_changelog()) | ||
Simon Sapin
|
r48773 | } | ||
Simon Sapin
|
r48777 | pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> { | ||
Martin von Zweigbergk
|
r50072 | self.changelog.get_mut_or_init(|| self.new_changelog()) | ||
Simon Sapin
|
r48773 | } | ||
Martin von Zweigbergk
|
r49981 | fn new_manifestlog(&self) -> Result<Manifestlog, HgError> { | ||
Raphaël Gomès
|
r52084 | Manifestlog::open( | ||
&self.store_vfs(), | ||||
Raphaël Gomès
|
r52760 | self.default_revlog_options(RevlogType::Manifestlog)?, | ||
Raphaël Gomès
|
r52084 | ) | ||
Simon Sapin
|
r48773 | } | ||
Simon Sapin
|
r48777 | pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> { | ||
Martin von Zweigbergk
|
r50072 | self.manifestlog.get_or_init(|| self.new_manifestlog()) | ||
Simon Sapin
|
r48773 | } | ||
Simon Sapin
|
r48777 | pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> { | ||
Martin von Zweigbergk
|
r50072 | self.manifestlog.get_mut_or_init(|| self.new_manifestlog()) | ||
Simon Sapin
|
r48773 | } | ||
Simon Sapin
|
r48774 | |||
Simon Sapin
|
r48783 | /// Returns the manifest of the *changeset* with the given node ID | ||
Simon Sapin
|
r48778 | pub fn manifest_for_node( | ||
&self, | ||||
node: impl Into<NodePrefix>, | ||||
) -> Result<Manifest, RevlogError> { | ||||
Simon Sapin
|
r48783 | self.manifestlog()?.data_for_node( | ||
Simon Sapin
|
r48778 | self.changelog()? | ||
Simon Sapin
|
r48783 | .data_for_node(node.into())? | ||
Simon Sapin
|
r48778 | .manifest_node()? | ||
.into(), | ||||
) | ||||
} | ||||
Simon Sapin
|
r48783 | /// Returns the manifest of the *changeset* with the given revision number | ||
Simon Sapin
|
r48778 | pub fn manifest_for_rev( | ||
Simon Sapin
|
r48774 | &self, | ||
Raphaël Gomès
|
r51870 | revision: UncheckedRevision, | ||
Simon Sapin
|
r48774 | ) -> Result<Manifest, RevlogError> { | ||
Simon Sapin
|
r48783 | self.manifestlog()?.data_for_node( | ||
self.changelog()? | ||||
.data_for_rev(revision)? | ||||
.manifest_node()? | ||||
.into(), | ||||
Simon Sapin
|
r48778 | ) | ||
Simon Sapin
|
r48774 | } | ||
Simon Sapin
|
r48775 | |||
Simon Sapin
|
r49341 | pub fn has_subrepos(&self) -> Result<bool, DirstateError> { | ||
if let Some(entry) = self.dirstate_map()?.get(HgPath::new(".hgsub"))? { | ||||
Raphaël Gomès
|
r50027 | Ok(entry.tracked()) | ||
Simon Sapin
|
r49341 | } else { | ||
Ok(false) | ||||
} | ||||
} | ||||
Simon Sapin
|
r48777 | pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> { | ||
Raphaël Gomès
|
r52760 | Filelog::open( | ||
self, | ||||
path, | ||||
self.default_revlog_options(RevlogType::Filelog)?, | ||||
) | ||||
Simon Sapin
|
r48775 | } | ||
Simon Sapin
|
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()?; | ||||
Raphaël Gomès
|
r51551 | let (packed_dirstate, old_uuid_to_remove) = if self.use_dirstate_v2() { | ||
Raphaël Gomès
|
r51140 | let (identity, uuid, data_size) = | ||
self.get_dirstate_data_file_integrity()?; | ||||
let identity_changed = identity != map.old_identity(); | ||||
Raphaël Gomès
|
r51139 | let uuid_changed = uuid.as_deref() != map.old_uuid(); | ||
let data_length_changed = data_size != map.old_data_size(); | ||||
Raphaël Gomès
|
r51140 | if identity_changed || uuid_changed || data_length_changed { | ||
// If any of identity, uuid or length have changed since | ||||
// last disk read, don't write. | ||||
Raphaël Gomès
|
r51139 | // This is fine because either we're in a command that doesn't | ||
// write anything too important (like `hg status`), or we're in | ||||
// `hg add` and we're supposed to have taken the lock before | ||||
// reading anyway. | ||||
// | ||||
// TODO complain loudly if we've changed anything important | ||||
// without taking the lock. | ||||
// (see `hg help config.format.use-dirstate-tracked-hint`) | ||||
log::debug!( | ||||
"dirstate has changed since last read, not updating." | ||||
); | ||||
return Ok(()); | ||||
} | ||||
let uuid_opt = map.old_uuid(); | ||||
r51116 | let write_mode = if uuid_opt.is_some() { | |||
DirstateMapWriteMode::Auto | ||||
} else { | ||||
DirstateMapWriteMode::ForceNewDataFile | ||||
}; | ||||
Raphaël Gomès
|
r50037 | let (data, tree_metadata, append, old_data_size) = | ||
r51116 | map.pack_v2(write_mode)?; | |||
Raphaël Gomès
|
r50044 | |||
// Reuse the uuid, or generate a new one, keeping the old for | ||||
// deletion. | ||||
let (uuid, old_uuid) = match uuid_opt { | ||||
Some(uuid) => { | ||||
let as_str = std::str::from_utf8(uuid) | ||||
.map_err(|_| { | ||||
HgError::corrupted( | ||||
"non-UTF-8 dirstate data file ID", | ||||
) | ||||
})? | ||||
.to_owned(); | ||||
if append { | ||||
(as_str, None) | ||||
} else { | ||||
(DirstateDocket::new_uid(), Some(as_str)) | ||||
} | ||||
} | ||||
None => (DirstateDocket::new_uid(), None), | ||||
Simon Sapin
|
r49249 | }; | ||
Raphaël Gomès
|
r50044 | |||
Simon Sapin
|
r49249 | let data_filename = format!("dirstate.{}", uuid); | ||
let data_filename = self.hg_vfs().join(data_filename); | ||||
let mut options = std::fs::OpenOptions::new(); | ||||
Arseniy Alekseyev
|
r50097 | options.write(true); | ||
// Why are we not using the O_APPEND flag when appending? | ||||
// | ||||
// - O_APPEND makes it trickier to deal with garbage at the end of | ||||
// the file, left by a previous uncommitted transaction. By | ||||
// starting the write at [old_data_size] we make sure we erase | ||||
// all such garbage. | ||||
// | ||||
// - O_APPEND requires to special-case 0-byte writes, whereas we | ||||
// don't need that. | ||||
// | ||||
// - Some OSes have bugs in implementation O_APPEND: | ||||
// revlog.py talks about a Solaris bug, but we also saw some ZFS | ||||
// bug: https://github.com/openzfs/zfs/pull/3124, | ||||
// https://github.com/openzfs/zfs/issues/13370 | ||||
// | ||||
if !append { | ||||
Raphaël Gomès
|
r51108 | log::trace!("creating a new dirstate data file"); | ||
Arseniy Alekseyev
|
r50097 | options.create_new(true); | ||
Raphaël Gomès
|
r51108 | } else { | ||
log::trace!("appending to the dirstate data file"); | ||||
Simon Sapin
|
r49249 | } | ||
Arseniy Alekseyev
|
r50097 | |||
Simon Sapin
|
r49249 | 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)?; | ||||
Arseniy Alekseyev
|
r50097 | if append { | ||
file.seek(SeekFrom::Start(old_data_size as u64))?; | ||||
Raphaël Gomès
|
r50037 | } | ||
Arseniy Alekseyev
|
r50097 | file.write_all(&data)?; | ||
file.flush()?; | ||||
Raphaël Gomès
|
r52013 | file.stream_position() | ||
Simon Sapin
|
r49249 | })() | ||
.when_writing_file(&data_filename)?; | ||||
Raphaël Gomès
|
r50038 | |||
let packed_dirstate = DirstateDocket::serialize( | ||||
Simon Sapin
|
r49249 | parents, | ||
tree_metadata, | ||||
data_size, | ||||
uuid.as_bytes(), | ||||
) | ||||
.map_err(|_: std::num::TryFromIntError| { | ||||
HgError::corrupted("overflow in dirstate docket serialization") | ||||
Raphaël Gomès
|
r50038 | })?; | ||
(packed_dirstate, old_uuid) | ||||
Simon Sapin
|
r49249 | } else { | ||
Raphaël Gomès
|
r51140 | let identity = self.dirstate_identity()?; | ||
if identity != map.old_identity() { | ||||
// If identity changed since last disk read, don't write. | ||||
// This is fine because either we're in a command that doesn't | ||||
// write anything too important (like `hg status`), or we're in | ||||
// `hg add` and we're supposed to have taken the lock before | ||||
// reading anyway. | ||||
// | ||||
// TODO complain loudly if we've changed anything important | ||||
// without taking the lock. | ||||
// (see `hg help config.format.use-dirstate-tracked-hint`) | ||||
log::debug!( | ||||
"dirstate has changed since last read, not updating." | ||||
); | ||||
return Ok(()); | ||||
} | ||||
Raphaël Gomès
|
r50038 | (map.pack_v1(parents)?, None) | ||
Simon Sapin
|
r49249 | }; | ||
Raphaël Gomès
|
r50038 | |||
let vfs = self.hg_vfs(); | ||||
vfs.atomic_write("dirstate", &packed_dirstate)?; | ||||
if let Some(uuid) = old_uuid_to_remove { | ||||
// Remove the old data file after the new docket pointing to the | ||||
// new data file was written. | ||||
vfs.remove_file(format!("dirstate.{}", uuid))?; | ||||
} | ||||
Simon Sapin
|
r49249 | Ok(()) | ||
} | ||||
Raphaël Gomès
|
r52084 | |||
pub fn default_revlog_options( | ||||
&self, | ||||
Raphaël Gomès
|
r52760 | revlog_type: RevlogType, | ||
Raphaël Gomès
|
r52084 | ) -> Result<RevlogOpenOptions, HgError> { | ||
let requirements = self.requirements(); | ||||
Raphaël Gomès
|
r52760 | let is_changelog = revlog_type == RevlogType::Changelog; | ||
let version = if is_changelog | ||||
Raphaël Gomès
|
r52084 | && requirements.contains(CHANGELOGV2_REQUIREMENT) | ||
{ | ||||
let compute_rank = self | ||||
.config() | ||||
.get_bool(b"experimental", b"changelog-v2.compute-rank")?; | ||||
RevlogVersionOptions::ChangelogV2 { compute_rank } | ||||
} else if requirements.contains(REVLOGV2_REQUIREMENT) { | ||||
RevlogVersionOptions::V2 | ||||
} else if requirements.contains(REVLOGV1_REQUIREMENT) { | ||||
RevlogVersionOptions::V1 { | ||||
Raphaël Gomès
|
r52760 | general_delta: requirements.contains(GENERALDELTA_REQUIREMENT), | ||
inline: !is_changelog, | ||||
Raphaël Gomès
|
r52084 | } | ||
} else { | ||||
RevlogVersionOptions::V0 | ||||
}; | ||||
Ok(RevlogOpenOptions { | ||||
version, | ||||
// We don't need to dance around the slow path like in the Python | ||||
// implementation since we know we have access to the fast code. | ||||
use_nodemap: requirements.contains(NODEMAP_REQUIREMENT), | ||||
Raphaël Gomès
|
r52760 | delta_config: RevlogDeltaConfig::new( | ||
self.config(), | ||||
self.requirements(), | ||||
revlog_type, | ||||
)?, | ||||
data_config: RevlogDataConfig::new( | ||||
self.config(), | ||||
self.requirements(), | ||||
)?, | ||||
feature_config: RevlogFeatureConfig::new( | ||||
self.config(), | ||||
requirements, | ||||
)?, | ||||
Raphaël Gomès
|
r52084 | }) | ||
} | ||||
Raphaël Gomès
|
r52928 | |||
pub fn node(&self, rev: UncheckedRevision) -> Option<crate::Node> { | ||||
self.changelog() | ||||
.ok() | ||||
.and_then(|c| c.node_from_rev(rev).copied()) | ||||
} | ||||
Simon Sapin
|
r48772 | } | ||
/// Lazily-initialized component of `Repo` with interior mutability | ||||
/// | ||||
/// This differs from `OnceCell` in that the value can still be "deinitialized" | ||||
Martin von Zweigbergk
|
r50072 | /// later by setting its inner `Option` to `None`. It also takes the | ||
/// initialization function as an argument when the value is requested, not | ||||
/// when the instance is created. | ||||
struct LazyCell<T> { | ||||
Simon Sapin
|
r48772 | value: RefCell<Option<T>>, | ||
} | ||||
Martin von Zweigbergk
|
r50072 | impl<T> LazyCell<T> { | ||
fn new() -> Self { | ||||
Simon Sapin
|
r48772 | Self { | ||
value: RefCell::new(None), | ||||
} | ||||
} | ||||
Simon Sapin
|
r49247 | fn set(&self, value: T) { | ||
*self.value.borrow_mut() = Some(value) | ||||
} | ||||
Martin von Zweigbergk
|
r50072 | fn get_or_init<E>( | ||
&self, | ||||
init: impl Fn() -> Result<T, E>, | ||||
) -> Result<Ref<T>, E> { | ||||
Simon Sapin
|
r48772 | let mut borrowed = self.value.borrow(); | ||
Simon Sapin
|
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. | ||||
Martin von Zweigbergk
|
r50072 | *self.value.borrow_mut() = Some(init()?); | ||
Simon Sapin
|
r48772 | borrowed = self.value.borrow() | ||
Simon Sapin
|
r48768 | } | ||
Ok(Ref::map(borrowed, |option| option.as_ref().unwrap())) | ||||
} | ||||
Martin von Zweigbergk
|
r50072 | fn get_mut_or_init<E>( | ||
&self, | ||||
init: impl Fn() -> Result<T, E>, | ||||
) -> Result<RefMut<T>, E> { | ||||
Simon Sapin
|
r48772 | let mut borrowed = self.value.borrow_mut(); | ||
Simon Sapin
|
r48768 | if borrowed.is_none() { | ||
Martin von Zweigbergk
|
r50072 | *borrowed = Some(init()?); | ||
Simon Sapin
|
r48768 | } | ||
Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap())) | ||||
} | ||||
Simon Sapin
|
r46782 | } | ||