##// END OF EJS Templates
rust: Move lazy initialization of `Repo::dirstate_map` into a generic struct...
Simon Sapin -
r48772:fc208d6f default
parent child Browse files
Show More
@@ -1,339 +1,367 b''
1 1 use crate::config::{Config, ConfigError, ConfigParseError};
2 2 use crate::dirstate::DirstateParents;
3 3 use crate::dirstate_tree::dirstate_map::DirstateMap;
4 4 use crate::dirstate_tree::owning::OwningDirstateMap;
5 5 use crate::errors::HgError;
6 6 use crate::errors::HgResultExt;
7 7 use crate::exit_codes;
8 8 use crate::requirements;
9 9 use crate::utils::files::get_path_from_bytes;
10 10 use crate::utils::SliceExt;
11 11 use crate::vfs::{is_dir, is_file, Vfs};
12 12 use crate::DirstateError;
13 13 use std::cell::{Cell, Ref, RefCell, RefMut};
14 14 use std::collections::HashSet;
15 15 use std::path::{Path, PathBuf};
16 16
17 17 /// A repository on disk
18 18 pub struct Repo {
19 19 working_directory: PathBuf,
20 20 dot_hg: PathBuf,
21 21 store: PathBuf,
22 22 requirements: HashSet<String>,
23 23 config: Config,
24 24 // None means not known/initialized yet
25 25 dirstate_parents: Cell<Option<DirstateParents>>,
26 dirstate_map: RefCell<Option<OwningDirstateMap>>,
26 dirstate_map: LazyCell<OwningDirstateMap, DirstateError>,
27 27 }
28 28
29 29 #[derive(Debug, derive_more::From)]
30 30 pub enum RepoError {
31 31 NotFound {
32 32 at: PathBuf,
33 33 },
34 34 #[from]
35 35 ConfigParseError(ConfigParseError),
36 36 #[from]
37 37 Other(HgError),
38 38 }
39 39
40 40 impl From<ConfigError> for RepoError {
41 41 fn from(error: ConfigError) -> Self {
42 42 match error {
43 43 ConfigError::Parse(error) => error.into(),
44 44 ConfigError::Other(error) => error.into(),
45 45 }
46 46 }
47 47 }
48 48
49 49 impl Repo {
50 50 /// tries to find nearest repository root in current working directory or
51 51 /// its ancestors
52 52 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
53 53 let current_directory = crate::utils::current_dir()?;
54 54 // ancestors() is inclusive: it first yields `current_directory`
55 55 // as-is.
56 56 for ancestor in current_directory.ancestors() {
57 57 if is_dir(ancestor.join(".hg"))? {
58 58 return Ok(ancestor.to_path_buf());
59 59 }
60 60 }
61 61 return Err(RepoError::NotFound {
62 62 at: current_directory,
63 63 });
64 64 }
65 65
66 66 /// Find a repository, either at the given path (which must contain a `.hg`
67 67 /// sub-directory) or by searching the current directory and its
68 68 /// ancestors.
69 69 ///
70 70 /// A method with two very different "modes" like this usually a code smell
71 71 /// to make two methods instead, but in this case an `Option` is what rhg
72 72 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
73 73 /// Having two methods would just move that `if` to almost all callers.
74 74 pub fn find(
75 75 config: &Config,
76 76 explicit_path: Option<PathBuf>,
77 77 ) -> Result<Self, RepoError> {
78 78 if let Some(root) = explicit_path {
79 79 if is_dir(root.join(".hg"))? {
80 80 Self::new_at_path(root.to_owned(), config)
81 81 } else if is_file(&root)? {
82 82 Err(HgError::unsupported("bundle repository").into())
83 83 } else {
84 84 Err(RepoError::NotFound {
85 85 at: root.to_owned(),
86 86 })
87 87 }
88 88 } else {
89 89 let root = Self::find_repo_root()?;
90 90 Self::new_at_path(root, config)
91 91 }
92 92 }
93 93
94 94 /// To be called after checking that `.hg` is a sub-directory
95 95 fn new_at_path(
96 96 working_directory: PathBuf,
97 97 config: &Config,
98 98 ) -> Result<Self, RepoError> {
99 99 let dot_hg = working_directory.join(".hg");
100 100
101 101 let mut repo_config_files = Vec::new();
102 102 repo_config_files.push(dot_hg.join("hgrc"));
103 103 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
104 104
105 105 let hg_vfs = Vfs { base: &dot_hg };
106 106 let mut reqs = requirements::load_if_exists(hg_vfs)?;
107 107 let relative =
108 108 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
109 109 let shared =
110 110 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
111 111
112 112 // From `mercurial/localrepo.py`:
113 113 //
114 114 // if .hg/requires contains the sharesafe requirement, it means
115 115 // there exists a `.hg/store/requires` too and we should read it
116 116 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
117 117 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
118 118 // is not present, refer checkrequirementscompat() for that
119 119 //
120 120 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
121 121 // repository was shared the old way. We check the share source
122 122 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
123 123 // current repository needs to be reshared
124 124 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
125 125
126 126 let store_path;
127 127 if !shared {
128 128 store_path = dot_hg.join("store");
129 129 } else {
130 130 let bytes = hg_vfs.read("sharedpath")?;
131 131 let mut shared_path =
132 132 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
133 133 .to_owned();
134 134 if relative {
135 135 shared_path = dot_hg.join(shared_path)
136 136 }
137 137 if !is_dir(&shared_path)? {
138 138 return Err(HgError::corrupted(format!(
139 139 ".hg/sharedpath points to nonexistent directory {}",
140 140 shared_path.display()
141 141 ))
142 142 .into());
143 143 }
144 144
145 145 store_path = shared_path.join("store");
146 146
147 147 let source_is_share_safe =
148 148 requirements::load(Vfs { base: &shared_path })?
149 149 .contains(requirements::SHARESAFE_REQUIREMENT);
150 150
151 151 if share_safe && !source_is_share_safe {
152 152 return Err(match config
153 153 .get(b"share", b"safe-mismatch.source-not-safe")
154 154 {
155 155 Some(b"abort") | None => HgError::abort(
156 156 "abort: share source does not support share-safe requirement\n\
157 157 (see `hg help config.format.use-share-safe` for more information)",
158 158 exit_codes::ABORT,
159 159 ),
160 160 _ => HgError::unsupported("share-safe downgrade"),
161 161 }
162 162 .into());
163 163 } else if source_is_share_safe && !share_safe {
164 164 return Err(
165 165 match config.get(b"share", b"safe-mismatch.source-safe") {
166 166 Some(b"abort") | None => HgError::abort(
167 167 "abort: version mismatch: source uses share-safe \
168 168 functionality while the current share does not\n\
169 169 (see `hg help config.format.use-share-safe` for more information)",
170 170 exit_codes::ABORT,
171 171 ),
172 172 _ => HgError::unsupported("share-safe upgrade"),
173 173 }
174 174 .into(),
175 175 );
176 176 }
177 177
178 178 if share_safe {
179 179 repo_config_files.insert(0, shared_path.join("hgrc"))
180 180 }
181 181 }
182 182 if share_safe {
183 183 reqs.extend(requirements::load(Vfs { base: &store_path })?);
184 184 }
185 185
186 186 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
187 187 config.combine_with_repo(&repo_config_files)?
188 188 } else {
189 189 config.clone()
190 190 };
191 191
192 192 let repo = Self {
193 193 requirements: reqs,
194 194 working_directory,
195 195 store: store_path,
196 196 dot_hg,
197 197 config: repo_config,
198 198 dirstate_parents: Cell::new(None),
199 dirstate_map: RefCell::new(None),
199 dirstate_map: LazyCell::new(Self::new_dirstate_map),
200 200 };
201 201
202 202 requirements::check(&repo)?;
203 203
204 204 Ok(repo)
205 205 }
206 206
207 207 pub fn working_directory_path(&self) -> &Path {
208 208 &self.working_directory
209 209 }
210 210
211 211 pub fn requirements(&self) -> &HashSet<String> {
212 212 &self.requirements
213 213 }
214 214
215 215 pub fn config(&self) -> &Config {
216 216 &self.config
217 217 }
218 218
219 219 /// For accessing repository files (in `.hg`), except for the store
220 220 /// (`.hg/store`).
221 221 pub fn hg_vfs(&self) -> Vfs<'_> {
222 222 Vfs { base: &self.dot_hg }
223 223 }
224 224
225 225 /// For accessing repository store files (in `.hg/store`)
226 226 pub fn store_vfs(&self) -> Vfs<'_> {
227 227 Vfs { base: &self.store }
228 228 }
229 229
230 230 /// For accessing the working copy
231 231 pub fn working_directory_vfs(&self) -> Vfs<'_> {
232 232 Vfs {
233 233 base: &self.working_directory,
234 234 }
235 235 }
236 236
237 237 pub fn has_dirstate_v2(&self) -> bool {
238 238 self.requirements
239 239 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
240 240 }
241 241
242 242 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
243 243 Ok(self
244 244 .hg_vfs()
245 245 .read("dirstate")
246 246 .io_not_found_as_none()?
247 247 .unwrap_or(Vec::new()))
248 248 }
249 249
250 250 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
251 251 if let Some(parents) = self.dirstate_parents.get() {
252 252 return Ok(parents);
253 253 }
254 254 let dirstate = self.dirstate_file_contents()?;
255 255 let parents = if dirstate.is_empty() {
256 256 DirstateParents::NULL
257 257 } else if self.has_dirstate_v2() {
258 258 crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents()
259 259 } else {
260 260 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
261 261 .clone()
262 262 };
263 263 self.dirstate_parents.set(Some(parents));
264 264 Ok(parents)
265 265 }
266 266
267 267 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
268 268 let dirstate_file_contents = self.dirstate_file_contents()?;
269 269 if dirstate_file_contents.is_empty() {
270 270 self.dirstate_parents.set(Some(DirstateParents::NULL));
271 271 Ok(OwningDirstateMap::new_empty(Vec::new()))
272 272 } else if self.has_dirstate_v2() {
273 273 let docket = crate::dirstate_tree::on_disk::read_docket(
274 274 &dirstate_file_contents,
275 275 )?;
276 276 self.dirstate_parents.set(Some(docket.parents()));
277 277 let data_size = docket.data_size();
278 278 let metadata = docket.tree_metadata();
279 279 let mut map = if let Some(data_mmap) = self
280 280 .hg_vfs()
281 281 .mmap_open(docket.data_filename())
282 282 .io_not_found_as_none()?
283 283 {
284 284 OwningDirstateMap::new_empty(MmapWrapper(data_mmap))
285 285 } else {
286 286 OwningDirstateMap::new_empty(Vec::new())
287 287 };
288 288 let (on_disk, placeholder) = map.get_mut_pair();
289 289 *placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?;
290 290 Ok(map)
291 291 } else {
292 292 let mut map = OwningDirstateMap::new_empty(dirstate_file_contents);
293 293 let (on_disk, placeholder) = map.get_mut_pair();
294 294 let (inner, parents) = DirstateMap::new_v1(on_disk)?;
295 295 self.dirstate_parents
296 296 .set(Some(parents.unwrap_or(DirstateParents::NULL)));
297 297 *placeholder = inner;
298 298 Ok(map)
299 299 }
300 300 }
301 301
302 302 pub fn dirstate_map(
303 303 &self,
304 304 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
305 let mut borrowed = self.dirstate_map.borrow();
305 self.dirstate_map.get_or_init(self)
306 }
307
308 pub fn dirstate_map_mut(
309 &self,
310 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
311 self.dirstate_map.get_mut_or_init(self)
312 }
313 }
314
315 /// Lazily-initialized component of `Repo` with interior mutability
316 ///
317 /// This differs from `OnceCell` in that the value can still be "deinitialized"
318 /// later by setting its inner `Option` to `None`.
319 struct LazyCell<T, E> {
320 value: RefCell<Option<T>>,
321 // `Fn`s that don’t capture environment are zero-size, so this box does
322 // not allocate:
323 init: Box<dyn Fn(&Repo) -> Result<T, E>>,
324 }
325
326 impl<T, E> LazyCell<T, E> {
327 fn new(init: impl Fn(&Repo) -> Result<T, E> + 'static) -> Self {
328 Self {
329 value: RefCell::new(None),
330 init: Box::new(init),
331 }
332 }
333
334 fn get_or_init(&self, repo: &Repo) -> Result<Ref<T>, E> {
335 let mut borrowed = self.value.borrow();
306 336 if borrowed.is_none() {
307 337 drop(borrowed);
308 338 // Only use `borrow_mut` if it is really needed to avoid panic in
309 339 // case there is another outstanding borrow but mutation is not
310 340 // needed.
311 *self.dirstate_map.borrow_mut() = Some(self.new_dirstate_map()?);
312 borrowed = self.dirstate_map.borrow()
341 *self.value.borrow_mut() = Some((self.init)(repo)?);
342 borrowed = self.value.borrow()
313 343 }
314 344 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
315 345 }
316 346
317 pub fn dirstate_map_mut(
318 &self,
319 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
320 let mut borrowed = self.dirstate_map.borrow_mut();
347 pub fn get_mut_or_init(&self, repo: &Repo) -> Result<RefMut<T>, E> {
348 let mut borrowed = self.value.borrow_mut();
321 349 if borrowed.is_none() {
322 *borrowed = Some(self.new_dirstate_map()?);
350 *borrowed = Some((self.init)(repo)?);
323 351 }
324 352 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
325 353 }
326 354 }
327 355
328 356 // TODO: remove this when https://github.com/RazrFalcon/memmap2-rs/pull/22 is on crates.io
329 357 struct MmapWrapper(memmap2::Mmap);
330 358
331 359 impl std::ops::Deref for MmapWrapper {
332 360 type Target = [u8];
333 361
334 362 fn deref(&self) -> &[u8] {
335 363 self.0.deref()
336 364 }
337 365 }
338 366
339 367 unsafe impl stable_deref_trait::StableDeref for MmapWrapper {}
General Comments 0
You need to be logged in to leave comments. Login now