Mercurial > public > mercurial-scm > hg-stable
comparison rust/hg-core/src/dirstate_tree/status.rs @ 47353:73ddcedeaadf
dirstate-tree: Change status() results to not borrow DirstateMap
The `status` function takes a `&'tree mut DirstateMap<'on_disk>` parameter.
`'on_disk` borrows a read-only byte buffer with the contents of the
`.hg/dirstate` file. `DirstateMap` internally uses represents file paths as
`std::borrow::Cow<'on_disk, HgPath>`, which borrows the byte buffer when
possible and allocates an owned string if not, such as for files added to the
dirstate after it was loaded from disk.
Previously the return type of of `status` has a `'tree`?lifetime, meaning it
could borrow all paths from the `DirstateMap`. With this changeset, that
lifetime is changed to `'on_disk` meaning that only paths from the byte buffer
can be borrowed, and paths allocated by `DirstateMap` must be copied.
Usually most paths are in the byte buffer, and most paths are not part of the
return value of `status`, so the number of extra copies should be small.
This change will enable `status` to mutate the `DirstateMap` after it has
finished constructing its return value. Previously such mutation would be
prevented by possible on-going borrows.
Differential Revision: https://phab.mercurial-scm.org/D10824
author | Simon Sapin <simon.sapin@octobus.net> |
---|---|
date | Fri, 28 May 2021 20:07:27 +0200 |
parents | 5e12b6bfdd3e |
children | 7138c863d0a1 |
comparison
equal
deleted
inserted
replaced
47352:5e12b6bfdd3e | 47353:73ddcedeaadf |
---|---|
1 use crate::dirstate::status::IgnoreFnType; | 1 use crate::dirstate::status::IgnoreFnType; |
2 use crate::dirstate_tree::dirstate_map::BorrowedPath; | |
2 use crate::dirstate_tree::dirstate_map::ChildNodesRef; | 3 use crate::dirstate_tree::dirstate_map::ChildNodesRef; |
3 use crate::dirstate_tree::dirstate_map::DirstateMap; | 4 use crate::dirstate_tree::dirstate_map::DirstateMap; |
4 use crate::dirstate_tree::dirstate_map::NodeRef; | 5 use crate::dirstate_tree::dirstate_map::NodeRef; |
5 use crate::dirstate_tree::on_disk::DirstateV2ParseError; | 6 use crate::dirstate_tree::on_disk::DirstateV2ParseError; |
6 use crate::matchers::get_ignore_function; | 7 use crate::matchers::get_ignore_function; |
15 use crate::PatternFileWarning; | 16 use crate::PatternFileWarning; |
16 use crate::StatusError; | 17 use crate::StatusError; |
17 use crate::StatusOptions; | 18 use crate::StatusOptions; |
18 use micro_timer::timed; | 19 use micro_timer::timed; |
19 use rayon::prelude::*; | 20 use rayon::prelude::*; |
20 use std::borrow::Cow; | |
21 use std::io; | 21 use std::io; |
22 use std::path::Path; | 22 use std::path::Path; |
23 use std::path::PathBuf; | 23 use std::path::PathBuf; |
24 use std::sync::Mutex; | 24 use std::sync::Mutex; |
25 | 25 |
37 dmap: &'tree mut DirstateMap<'on_disk>, | 37 dmap: &'tree mut DirstateMap<'on_disk>, |
38 matcher: &(dyn Matcher + Sync), | 38 matcher: &(dyn Matcher + Sync), |
39 root_dir: PathBuf, | 39 root_dir: PathBuf, |
40 ignore_files: Vec<PathBuf>, | 40 ignore_files: Vec<PathBuf>, |
41 options: StatusOptions, | 41 options: StatusOptions, |
42 ) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> { | 42 ) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> { |
43 let (ignore_fn, warnings): (IgnoreFnType, _) = | 43 let (ignore_fn, warnings): (IgnoreFnType, _) = |
44 if options.list_ignored || options.list_unknown { | 44 if options.list_ignored || options.list_unknown { |
45 get_ignore_function(ignore_files, &root_dir)? | 45 get_ignore_function(ignore_files, &root_dir)? |
46 } else { | 46 } else { |
47 (Box::new(|&_| true), vec![]) | 47 (Box::new(|&_| true), vec![]) |
53 matcher, | 53 matcher, |
54 ignore_fn, | 54 ignore_fn, |
55 outcome: Mutex::new(DirstateStatus::default()), | 55 outcome: Mutex::new(DirstateStatus::default()), |
56 }; | 56 }; |
57 let is_at_repo_root = true; | 57 let is_at_repo_root = true; |
58 let hg_path = HgPath::new(""); | 58 let hg_path = &BorrowedPath::OnDisk(HgPath::new("")); |
59 let has_ignored_ancestor = false; | 59 let has_ignored_ancestor = false; |
60 common.traverse_fs_directory_and_dirstate( | 60 common.traverse_fs_directory_and_dirstate( |
61 has_ignored_ancestor, | 61 has_ignored_ancestor, |
62 dmap.root.as_ref(), | 62 dmap.root.as_ref(), |
63 hg_path, | 63 hg_path, |
67 Ok((common.outcome.into_inner().unwrap(), warnings)) | 67 Ok((common.outcome.into_inner().unwrap(), warnings)) |
68 } | 68 } |
69 | 69 |
70 /// Bag of random things needed by various parts of the algorithm. Reduces the | 70 /// Bag of random things needed by various parts of the algorithm. Reduces the |
71 /// number of parameters passed to functions. | 71 /// number of parameters passed to functions. |
72 struct StatusCommon<'tree, 'a, 'on_disk: 'tree> { | 72 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> { |
73 dmap: &'tree DirstateMap<'on_disk>, | 73 dmap: &'tree DirstateMap<'on_disk>, |
74 options: StatusOptions, | 74 options: StatusOptions, |
75 matcher: &'a (dyn Matcher + Sync), | 75 matcher: &'a (dyn Matcher + Sync), |
76 ignore_fn: IgnoreFnType<'a>, | 76 ignore_fn: IgnoreFnType<'a>, |
77 outcome: Mutex<DirstateStatus<'tree>>, | 77 outcome: Mutex<DirstateStatus<'on_disk>>, |
78 } | 78 } |
79 | 79 |
80 impl<'tree, 'a> StatusCommon<'tree, 'a, '_> { | 80 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> { |
81 fn read_dir( | 81 fn read_dir( |
82 &self, | 82 &self, |
83 hg_path: &HgPath, | 83 hg_path: &HgPath, |
84 fs_path: &Path, | 84 fs_path: &Path, |
85 is_at_repo_root: bool, | 85 is_at_repo_root: bool, |
98 } | 98 } |
99 | 99 |
100 fn traverse_fs_directory_and_dirstate( | 100 fn traverse_fs_directory_and_dirstate( |
101 &self, | 101 &self, |
102 has_ignored_ancestor: bool, | 102 has_ignored_ancestor: bool, |
103 dirstate_nodes: ChildNodesRef<'tree, '_>, | 103 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>, |
104 directory_hg_path: &'tree HgPath, | 104 directory_hg_path: &BorrowedPath<'tree, 'on_disk>, |
105 directory_fs_path: &Path, | 105 directory_fs_path: &Path, |
106 is_at_repo_root: bool, | 106 is_at_repo_root: bool, |
107 ) -> Result<(), DirstateV2ParseError> { | 107 ) -> Result<(), DirstateV2ParseError> { |
108 if !self.options.list_unknown && !self.options.list_ignored { | 108 if !self.options.list_unknown && !self.options.list_ignored { |
109 // We only care about files in the dirstate, so we can skip listing | 109 // We only care about files in the dirstate, so we can skip listing |
197 | 197 |
198 fn traverse_fs_and_dirstate( | 198 fn traverse_fs_and_dirstate( |
199 &self, | 199 &self, |
200 fs_path: &Path, | 200 fs_path: &Path, |
201 fs_metadata: &std::fs::Metadata, | 201 fs_metadata: &std::fs::Metadata, |
202 dirstate_node: NodeRef<'tree, '_>, | 202 dirstate_node: NodeRef<'tree, 'on_disk>, |
203 has_ignored_ancestor: bool, | 203 has_ignored_ancestor: bool, |
204 ) -> Result<(), DirstateV2ParseError> { | 204 ) -> Result<(), DirstateV2ParseError> { |
205 let hg_path = dirstate_node.full_path(self.dmap.on_disk)?; | 205 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?; |
206 let file_type = fs_metadata.file_type(); | 206 let file_type = fs_metadata.file_type(); |
207 let file_or_symlink = file_type.is_file() || file_type.is_symlink(); | 207 let file_or_symlink = file_type.is_file() || file_type.is_symlink(); |
208 if !file_or_symlink { | 208 if !file_or_symlink { |
209 // If we previously had a file here, it was removed (with | 209 // If we previously had a file here, it was removed (with |
210 // `hg rm` or similar) or deleted before it could be | 210 // `hg rm` or similar) or deleted before it could be |
211 // replaced by a directory or something else. | 211 // replaced by a directory or something else. |
212 self.mark_removed_or_deleted_if_file( | 212 self.mark_removed_or_deleted_if_file( |
213 hg_path, | 213 &hg_path, |
214 dirstate_node.state()?, | 214 dirstate_node.state()?, |
215 ); | 215 ); |
216 } | 216 } |
217 if file_type.is_dir() { | 217 if file_type.is_dir() { |
218 if self.options.collect_traversed_dirs { | 218 if self.options.collect_traversed_dirs { |
219 self.outcome.lock().unwrap().traversed.push(hg_path.into()) | 219 self.outcome |
220 .lock() | |
221 .unwrap() | |
222 .traversed | |
223 .push(hg_path.detach_from_tree()) | |
220 } | 224 } |
221 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path); | 225 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path); |
222 let is_at_repo_root = false; | 226 let is_at_repo_root = false; |
223 self.traverse_fs_directory_and_dirstate( | 227 self.traverse_fs_directory_and_dirstate( |
224 is_ignored, | 228 is_ignored, |
227 fs_path, | 231 fs_path, |
228 is_at_repo_root, | 232 is_at_repo_root, |
229 )? | 233 )? |
230 } else { | 234 } else { |
231 if file_or_symlink && self.matcher.matches(hg_path) { | 235 if file_or_symlink && self.matcher.matches(hg_path) { |
232 let full_path = Cow::from(hg_path); | |
233 if let Some(state) = dirstate_node.state()? { | 236 if let Some(state) = dirstate_node.state()? { |
234 match state { | 237 match state { |
235 EntryState::Added => { | 238 EntryState::Added => self |
236 self.outcome.lock().unwrap().added.push(full_path) | 239 .outcome |
237 } | 240 .lock() |
241 .unwrap() | |
242 .added | |
243 .push(hg_path.detach_from_tree()), | |
238 EntryState::Removed => self | 244 EntryState::Removed => self |
239 .outcome | 245 .outcome |
240 .lock() | 246 .lock() |
241 .unwrap() | 247 .unwrap() |
242 .removed | 248 .removed |
243 .push(full_path), | 249 .push(hg_path.detach_from_tree()), |
244 EntryState::Merged => self | 250 EntryState::Merged => self |
245 .outcome | 251 .outcome |
246 .lock() | 252 .lock() |
247 .unwrap() | 253 .unwrap() |
248 .modified | 254 .modified |
249 .push(full_path), | 255 .push(hg_path.detach_from_tree()), |
250 EntryState::Normal => self | 256 EntryState::Normal => self |
251 .handle_normal_file(&dirstate_node, fs_metadata)?, | 257 .handle_normal_file(&dirstate_node, fs_metadata)?, |
252 // This variant is not used in DirstateMap | 258 // This variant is not used in DirstateMap |
253 // nodes | 259 // nodes |
254 EntryState::Unknown => unreachable!(), | 260 EntryState::Unknown => unreachable!(), |
255 } | 261 } |
256 } else { | 262 } else { |
257 // `node.entry.is_none()` indicates a "directory" | 263 // `node.entry.is_none()` indicates a "directory" |
258 // node, but the filesystem has a file | 264 // node, but the filesystem has a file |
259 self.mark_unknown_or_ignored( | 265 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path) |
260 has_ignored_ancestor, | |
261 full_path, | |
262 ) | |
263 } | 266 } |
264 } | 267 } |
265 | 268 |
266 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter() | 269 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter() |
267 { | 270 { |
273 | 276 |
274 /// A file with `EntryState::Normal` in the dirstate was found in the | 277 /// A file with `EntryState::Normal` in the dirstate was found in the |
275 /// filesystem | 278 /// filesystem |
276 fn handle_normal_file( | 279 fn handle_normal_file( |
277 &self, | 280 &self, |
278 dirstate_node: &NodeRef<'tree, '_>, | 281 dirstate_node: &NodeRef<'tree, 'on_disk>, |
279 fs_metadata: &std::fs::Metadata, | 282 fs_metadata: &std::fs::Metadata, |
280 ) -> Result<(), DirstateV2ParseError> { | 283 ) -> Result<(), DirstateV2ParseError> { |
281 // Keep the low 31 bits | 284 // Keep the low 31 bits |
282 fn truncate_u64(value: u64) -> i32 { | 285 fn truncate_u64(value: u64) -> i32 { |
283 (value & 0x7FFF_FFFF) as i32 | 286 (value & 0x7FFF_FFFF) as i32 |
287 } | 290 } |
288 | 291 |
289 let entry = dirstate_node | 292 let entry = dirstate_node |
290 .entry()? | 293 .entry()? |
291 .expect("handle_normal_file called with entry-less node"); | 294 .expect("handle_normal_file called with entry-less node"); |
292 let full_path = Cow::from(dirstate_node.full_path(self.dmap.on_disk)?); | 295 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?; |
293 let mode_changed = | 296 let mode_changed = |
294 || self.options.check_exec && entry.mode_changed(fs_metadata); | 297 || self.options.check_exec && entry.mode_changed(fs_metadata); |
295 let size_changed = entry.size != truncate_u64(fs_metadata.len()); | 298 let size_changed = entry.size != truncate_u64(fs_metadata.len()); |
296 if entry.size >= 0 | 299 if entry.size >= 0 |
297 && size_changed | 300 && size_changed |
298 && fs_metadata.file_type().is_symlink() | 301 && fs_metadata.file_type().is_symlink() |
299 { | 302 { |
300 // issue6456: Size returned may be longer due to encryption | 303 // issue6456: Size returned may be longer due to encryption |
301 // on EXT-4 fscrypt. TODO maybe only do it on EXT4? | 304 // on EXT-4 fscrypt. TODO maybe only do it on EXT4? |
302 self.outcome.lock().unwrap().unsure.push(full_path) | 305 self.outcome |
306 .lock() | |
307 .unwrap() | |
308 .unsure | |
309 .push(hg_path.detach_from_tree()) | |
303 } else if dirstate_node.has_copy_source() | 310 } else if dirstate_node.has_copy_source() |
304 || entry.is_from_other_parent() | 311 || entry.is_from_other_parent() |
305 || (entry.size >= 0 && (size_changed || mode_changed())) | 312 || (entry.size >= 0 && (size_changed || mode_changed())) |
306 { | 313 { |
307 self.outcome.lock().unwrap().modified.push(full_path) | 314 self.outcome |
315 .lock() | |
316 .unwrap() | |
317 .modified | |
318 .push(hg_path.detach_from_tree()) | |
308 } else { | 319 } else { |
309 let mtime = mtime_seconds(fs_metadata); | 320 let mtime = mtime_seconds(fs_metadata); |
310 if truncate_i64(mtime) != entry.mtime | 321 if truncate_i64(mtime) != entry.mtime |
311 || mtime == self.options.last_normal_time | 322 || mtime == self.options.last_normal_time |
312 { | 323 { |
313 self.outcome.lock().unwrap().unsure.push(full_path) | 324 self.outcome |
325 .lock() | |
326 .unwrap() | |
327 .unsure | |
328 .push(hg_path.detach_from_tree()) | |
314 } else if self.options.list_clean { | 329 } else if self.options.list_clean { |
315 self.outcome.lock().unwrap().clean.push(full_path) | 330 self.outcome |
331 .lock() | |
332 .unwrap() | |
333 .clean | |
334 .push(hg_path.detach_from_tree()) | |
316 } | 335 } |
317 } | 336 } |
318 Ok(()) | 337 Ok(()) |
319 } | 338 } |
320 | 339 |
321 /// A node in the dirstate tree has no corresponding filesystem entry | 340 /// A node in the dirstate tree has no corresponding filesystem entry |
322 fn traverse_dirstate_only( | 341 fn traverse_dirstate_only( |
323 &self, | 342 &self, |
324 dirstate_node: NodeRef<'tree, '_>, | 343 dirstate_node: NodeRef<'tree, 'on_disk>, |
325 ) -> Result<(), DirstateV2ParseError> { | 344 ) -> Result<(), DirstateV2ParseError> { |
326 self.mark_removed_or_deleted_if_file( | 345 self.mark_removed_or_deleted_if_file( |
327 dirstate_node.full_path(self.dmap.on_disk)?, | 346 &dirstate_node.full_path_borrowed(self.dmap.on_disk)?, |
328 dirstate_node.state()?, | 347 dirstate_node.state()?, |
329 ); | 348 ); |
330 dirstate_node | 349 dirstate_node |
331 .children(self.dmap.on_disk)? | 350 .children(self.dmap.on_disk)? |
332 .par_iter() | 351 .par_iter() |
338 /// filesystem | 357 /// filesystem |
339 /// | 358 /// |
340 /// Does nothing on a "directory" node | 359 /// Does nothing on a "directory" node |
341 fn mark_removed_or_deleted_if_file( | 360 fn mark_removed_or_deleted_if_file( |
342 &self, | 361 &self, |
343 hg_path: &'tree HgPath, | 362 hg_path: &BorrowedPath<'tree, 'on_disk>, |
344 dirstate_node_state: Option<EntryState>, | 363 dirstate_node_state: Option<EntryState>, |
345 ) { | 364 ) { |
346 if let Some(state) = dirstate_node_state { | 365 if let Some(state) = dirstate_node_state { |
347 if self.matcher.matches(hg_path) { | 366 if self.matcher.matches(hg_path) { |
348 if let EntryState::Removed = state { | 367 if let EntryState::Removed = state { |
349 self.outcome.lock().unwrap().removed.push(hg_path.into()) | 368 self.outcome |
369 .lock() | |
370 .unwrap() | |
371 .removed | |
372 .push(hg_path.detach_from_tree()) | |
350 } else { | 373 } else { |
351 self.outcome.lock().unwrap().deleted.push(hg_path.into()) | 374 self.outcome |
375 .lock() | |
376 .unwrap() | |
377 .deleted | |
378 .push(hg_path.detach_from_tree()) | |
352 } | 379 } |
353 } | 380 } |
354 } | 381 } |
355 } | 382 } |
356 | 383 |
393 } | 420 } |
394 if self.options.collect_traversed_dirs { | 421 if self.options.collect_traversed_dirs { |
395 self.outcome.lock().unwrap().traversed.push(hg_path.into()) | 422 self.outcome.lock().unwrap().traversed.push(hg_path.into()) |
396 } | 423 } |
397 } else if file_or_symlink && self.matcher.matches(&hg_path) { | 424 } else if file_or_symlink && self.matcher.matches(&hg_path) { |
398 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into()) | 425 self.mark_unknown_or_ignored( |
426 has_ignored_ancestor, | |
427 &BorrowedPath::InMemory(&hg_path), | |
428 ) | |
399 } | 429 } |
400 } | 430 } |
401 | 431 |
402 fn mark_unknown_or_ignored( | 432 fn mark_unknown_or_ignored( |
403 &self, | 433 &self, |
404 has_ignored_ancestor: bool, | 434 has_ignored_ancestor: bool, |
405 hg_path: Cow<'tree, HgPath>, | 435 hg_path: &BorrowedPath<'_, 'on_disk>, |
406 ) { | 436 ) { |
407 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path); | 437 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path); |
408 if is_ignored { | 438 if is_ignored { |
409 if self.options.list_ignored { | 439 if self.options.list_ignored { |
410 self.outcome.lock().unwrap().ignored.push(hg_path) | 440 self.outcome |
441 .lock() | |
442 .unwrap() | |
443 .ignored | |
444 .push(hg_path.detach_from_tree()) | |
411 } | 445 } |
412 } else { | 446 } else { |
413 if self.options.list_unknown { | 447 if self.options.list_unknown { |
414 self.outcome.lock().unwrap().unknown.push(hg_path) | 448 self.outcome |
449 .lock() | |
450 .unwrap() | |
451 .unknown | |
452 .push(hg_path.detach_from_tree()) | |
415 } | 453 } |
416 } | 454 } |
417 } | 455 } |
418 } | 456 } |
419 | 457 |