##// END OF EJS Templates
dirstate-v2: Drop parent directory cache when removing a dirstate node...
Simon Sapin -
r48141:9d58e54b default
parent child Browse files
Show More
@@ -1,1095 +1,1113 b''
1 1 use bytes_cast::BytesCast;
2 2 use micro_timer::timed;
3 3 use std::borrow::Cow;
4 4 use std::convert::TryInto;
5 5 use std::path::PathBuf;
6 6
7 7 use super::on_disk;
8 8 use super::on_disk::DirstateV2ParseError;
9 9 use super::path_with_basename::WithBasename;
10 10 use crate::dirstate::parsers::pack_entry;
11 11 use crate::dirstate::parsers::packed_entry_size;
12 12 use crate::dirstate::parsers::parse_dirstate_entries;
13 13 use crate::dirstate::parsers::Timestamp;
14 14 use crate::matchers::Matcher;
15 15 use crate::utils::hg_path::{HgPath, HgPathBuf};
16 16 use crate::CopyMapIter;
17 17 use crate::DirstateEntry;
18 18 use crate::DirstateError;
19 19 use crate::DirstateParents;
20 20 use crate::DirstateStatus;
21 21 use crate::EntryState;
22 22 use crate::FastHashMap;
23 23 use crate::PatternFileWarning;
24 24 use crate::StateMapIter;
25 25 use crate::StatusError;
26 26 use crate::StatusOptions;
27 27
28 28 pub struct DirstateMap<'on_disk> {
29 29 /// Contents of the `.hg/dirstate` file
30 30 pub(super) on_disk: &'on_disk [u8],
31 31
32 32 pub(super) root: ChildNodes<'on_disk>,
33 33
34 34 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
35 35 pub(super) nodes_with_entry_count: u32,
36 36
37 37 /// Number of nodes anywhere in the tree that have
38 38 /// `.copy_source.is_some()`.
39 39 pub(super) nodes_with_copy_source_count: u32,
40 40 }
41 41
42 42 /// Using a plain `HgPathBuf` of the full path from the repository root as a
43 43 /// map key would also work: all paths in a given map have the same parent
44 44 /// path, so comparing full paths gives the same result as comparing base
45 45 /// names. However `HashMap` would waste time always re-hashing the same
46 46 /// string prefix.
47 47 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
48 48
49 49 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
50 50 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
51 51 pub(super) enum BorrowedPath<'tree, 'on_disk> {
52 52 InMemory(&'tree HgPathBuf),
53 53 OnDisk(&'on_disk HgPath),
54 54 }
55 55
56 56 pub(super) enum ChildNodes<'on_disk> {
57 57 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
58 58 OnDisk(&'on_disk [on_disk::Node]),
59 59 }
60 60
61 61 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
62 62 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
63 63 OnDisk(&'on_disk [on_disk::Node]),
64 64 }
65 65
66 66 pub(super) enum NodeRef<'tree, 'on_disk> {
67 67 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
68 68 OnDisk(&'on_disk on_disk::Node),
69 69 }
70 70
71 71 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
72 72 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
73 73 match *self {
74 74 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
75 75 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
76 76 }
77 77 }
78 78 }
79 79
80 80 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
81 81 type Target = HgPath;
82 82
83 83 fn deref(&self) -> &HgPath {
84 84 match *self {
85 85 BorrowedPath::InMemory(in_memory) => in_memory,
86 86 BorrowedPath::OnDisk(on_disk) => on_disk,
87 87 }
88 88 }
89 89 }
90 90
91 91 impl Default for ChildNodes<'_> {
92 92 fn default() -> Self {
93 93 ChildNodes::InMemory(Default::default())
94 94 }
95 95 }
96 96
97 97 impl<'on_disk> ChildNodes<'on_disk> {
98 98 pub(super) fn as_ref<'tree>(
99 99 &'tree self,
100 100 ) -> ChildNodesRef<'tree, 'on_disk> {
101 101 match self {
102 102 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
103 103 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
104 104 }
105 105 }
106 106
107 107 pub(super) fn is_empty(&self) -> bool {
108 108 match self {
109 109 ChildNodes::InMemory(nodes) => nodes.is_empty(),
110 110 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
111 111 }
112 112 }
113 113
114 114 pub(super) fn make_mut(
115 115 &mut self,
116 116 on_disk: &'on_disk [u8],
117 117 ) -> Result<
118 118 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
119 119 DirstateV2ParseError,
120 120 > {
121 121 match self {
122 122 ChildNodes::InMemory(nodes) => Ok(nodes),
123 123 ChildNodes::OnDisk(nodes) => {
124 124 let nodes = nodes
125 125 .iter()
126 126 .map(|node| {
127 127 Ok((
128 128 node.path(on_disk)?,
129 129 node.to_in_memory_node(on_disk)?,
130 130 ))
131 131 })
132 132 .collect::<Result<_, _>>()?;
133 133 *self = ChildNodes::InMemory(nodes);
134 134 match self {
135 135 ChildNodes::InMemory(nodes) => Ok(nodes),
136 136 ChildNodes::OnDisk(_) => unreachable!(),
137 137 }
138 138 }
139 139 }
140 140 }
141 141 }
142 142
143 143 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
144 144 pub(super) fn get(
145 145 &self,
146 146 base_name: &HgPath,
147 147 on_disk: &'on_disk [u8],
148 148 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
149 149 match self {
150 150 ChildNodesRef::InMemory(nodes) => Ok(nodes
151 151 .get_key_value(base_name)
152 152 .map(|(k, v)| NodeRef::InMemory(k, v))),
153 153 ChildNodesRef::OnDisk(nodes) => {
154 154 let mut parse_result = Ok(());
155 155 let search_result = nodes.binary_search_by(|node| {
156 156 match node.base_name(on_disk) {
157 157 Ok(node_base_name) => node_base_name.cmp(base_name),
158 158 Err(e) => {
159 159 parse_result = Err(e);
160 160 // Dummy comparison result, `search_result` won’t
161 161 // be used since `parse_result` is an error
162 162 std::cmp::Ordering::Equal
163 163 }
164 164 }
165 165 });
166 166 parse_result.map(|()| {
167 167 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
168 168 })
169 169 }
170 170 }
171 171 }
172 172
173 173 /// Iterate in undefined order
174 174 pub(super) fn iter(
175 175 &self,
176 176 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
177 177 match self {
178 178 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
179 179 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
180 180 ),
181 181 ChildNodesRef::OnDisk(nodes) => {
182 182 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
183 183 }
184 184 }
185 185 }
186 186
187 187 /// Iterate in parallel in undefined order
188 188 pub(super) fn par_iter(
189 189 &self,
190 190 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
191 191 {
192 192 use rayon::prelude::*;
193 193 match self {
194 194 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
195 195 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
196 196 ),
197 197 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
198 198 nodes.par_iter().map(NodeRef::OnDisk),
199 199 ),
200 200 }
201 201 }
202 202
203 203 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
204 204 match self {
205 205 ChildNodesRef::InMemory(nodes) => {
206 206 let mut vec: Vec<_> = nodes
207 207 .iter()
208 208 .map(|(k, v)| NodeRef::InMemory(k, v))
209 209 .collect();
210 210 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
211 211 match node {
212 212 NodeRef::InMemory(path, _node) => path.base_name(),
213 213 NodeRef::OnDisk(_) => unreachable!(),
214 214 }
215 215 }
216 216 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
217 217 // value: https://github.com/rust-lang/rust/issues/34162
218 218 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
219 219 vec
220 220 }
221 221 ChildNodesRef::OnDisk(nodes) => {
222 222 // Nodes on disk are already sorted
223 223 nodes.iter().map(NodeRef::OnDisk).collect()
224 224 }
225 225 }
226 226 }
227 227 }
228 228
229 229 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
230 230 pub(super) fn full_path(
231 231 &self,
232 232 on_disk: &'on_disk [u8],
233 233 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
234 234 match self {
235 235 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
236 236 NodeRef::OnDisk(node) => node.full_path(on_disk),
237 237 }
238 238 }
239 239
240 240 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
241 241 /// HgPath>` detached from `'tree`
242 242 pub(super) fn full_path_borrowed(
243 243 &self,
244 244 on_disk: &'on_disk [u8],
245 245 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
246 246 match self {
247 247 NodeRef::InMemory(path, _node) => match path.full_path() {
248 248 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
249 249 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
250 250 },
251 251 NodeRef::OnDisk(node) => {
252 252 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
253 253 }
254 254 }
255 255 }
256 256
257 257 pub(super) fn base_name(
258 258 &self,
259 259 on_disk: &'on_disk [u8],
260 260 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
261 261 match self {
262 262 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
263 263 NodeRef::OnDisk(node) => node.base_name(on_disk),
264 264 }
265 265 }
266 266
267 267 pub(super) fn children(
268 268 &self,
269 269 on_disk: &'on_disk [u8],
270 270 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
271 271 match self {
272 272 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
273 273 NodeRef::OnDisk(node) => {
274 274 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
275 275 }
276 276 }
277 277 }
278 278
279 279 pub(super) fn has_copy_source(&self) -> bool {
280 280 match self {
281 281 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
282 282 NodeRef::OnDisk(node) => node.has_copy_source(),
283 283 }
284 284 }
285 285
286 286 pub(super) fn copy_source(
287 287 &self,
288 288 on_disk: &'on_disk [u8],
289 289 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
290 290 match self {
291 291 NodeRef::InMemory(_path, node) => {
292 292 Ok(node.copy_source.as_ref().map(|s| &**s))
293 293 }
294 294 NodeRef::OnDisk(node) => node.copy_source(on_disk),
295 295 }
296 296 }
297 297
298 298 pub(super) fn entry(
299 299 &self,
300 300 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
301 301 match self {
302 302 NodeRef::InMemory(_path, node) => {
303 303 Ok(node.data.as_entry().copied())
304 304 }
305 305 NodeRef::OnDisk(node) => node.entry(),
306 306 }
307 307 }
308 308
309 309 pub(super) fn state(
310 310 &self,
311 311 ) -> Result<Option<EntryState>, DirstateV2ParseError> {
312 312 match self {
313 313 NodeRef::InMemory(_path, node) => {
314 314 Ok(node.data.as_entry().map(|entry| entry.state))
315 315 }
316 316 NodeRef::OnDisk(node) => node.state(),
317 317 }
318 318 }
319 319
320 320 pub(super) fn cached_directory_mtime(
321 321 &self,
322 322 ) -> Option<&'tree on_disk::Timestamp> {
323 323 match self {
324 324 NodeRef::InMemory(_path, node) => match &node.data {
325 325 NodeData::CachedDirectory { mtime } => Some(mtime),
326 326 _ => None,
327 327 },
328 328 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
329 329 }
330 330 }
331 331
332 332 pub(super) fn tracked_descendants_count(&self) -> u32 {
333 333 match self {
334 334 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
335 335 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
336 336 }
337 337 }
338 338 }
339 339
340 340 /// Represents a file or a directory
341 341 #[derive(Default)]
342 342 pub(super) struct Node<'on_disk> {
343 343 pub(super) data: NodeData,
344 344
345 345 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
346 346
347 347 pub(super) children: ChildNodes<'on_disk>,
348 348
349 349 /// How many (non-inclusive) descendants of this node are tracked files
350 350 pub(super) tracked_descendants_count: u32,
351 351 }
352 352
353 353 pub(super) enum NodeData {
354 354 Entry(DirstateEntry),
355 355 CachedDirectory { mtime: on_disk::Timestamp },
356 356 None,
357 357 }
358 358
359 359 impl Default for NodeData {
360 360 fn default() -> Self {
361 361 NodeData::None
362 362 }
363 363 }
364 364
365 365 impl NodeData {
366 366 fn has_entry(&self) -> bool {
367 367 match self {
368 368 NodeData::Entry(_) => true,
369 369 _ => false,
370 370 }
371 371 }
372 372
373 373 fn as_entry(&self) -> Option<&DirstateEntry> {
374 374 match self {
375 375 NodeData::Entry(entry) => Some(entry),
376 376 _ => None,
377 377 }
378 378 }
379 379 }
380 380
381 381 impl<'on_disk> DirstateMap<'on_disk> {
382 382 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
383 383 Self {
384 384 on_disk,
385 385 root: ChildNodes::default(),
386 386 nodes_with_entry_count: 0,
387 387 nodes_with_copy_source_count: 0,
388 388 }
389 389 }
390 390
391 391 #[timed]
392 392 pub fn new_v2(
393 393 on_disk: &'on_disk [u8],
394 394 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
395 395 Ok(on_disk::read(on_disk)?)
396 396 }
397 397
398 398 #[timed]
399 399 pub fn new_v1(
400 400 on_disk: &'on_disk [u8],
401 401 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
402 402 let mut map = Self::empty(on_disk);
403 403 if map.on_disk.is_empty() {
404 404 return Ok((map, None));
405 405 }
406 406
407 407 let parents = parse_dirstate_entries(
408 408 map.on_disk,
409 409 |path, entry, copy_source| {
410 410 let tracked = entry.state.is_tracked();
411 411 let node = Self::get_or_insert_node(
412 412 map.on_disk,
413 413 &mut map.root,
414 414 path,
415 415 WithBasename::to_cow_borrowed,
416 416 |ancestor| {
417 417 if tracked {
418 418 ancestor.tracked_descendants_count += 1
419 419 }
420 420 },
421 421 )?;
422 422 assert!(
423 423 !node.data.has_entry(),
424 424 "duplicate dirstate entry in read"
425 425 );
426 426 assert!(
427 427 node.copy_source.is_none(),
428 428 "duplicate dirstate entry in read"
429 429 );
430 430 node.data = NodeData::Entry(*entry);
431 431 node.copy_source = copy_source.map(Cow::Borrowed);
432 432 map.nodes_with_entry_count += 1;
433 433 if copy_source.is_some() {
434 434 map.nodes_with_copy_source_count += 1
435 435 }
436 436 Ok(())
437 437 },
438 438 )?;
439 439 let parents = Some(parents.clone());
440 440
441 441 Ok((map, parents))
442 442 }
443 443
444 444 fn get_node<'tree>(
445 445 &'tree self,
446 446 path: &HgPath,
447 447 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
448 448 let mut children = self.root.as_ref();
449 449 let mut components = path.components();
450 450 let mut component =
451 451 components.next().expect("expected at least one components");
452 452 loop {
453 453 if let Some(child) = children.get(component, self.on_disk)? {
454 454 if let Some(next_component) = components.next() {
455 455 component = next_component;
456 456 children = child.children(self.on_disk)?;
457 457 } else {
458 458 return Ok(Some(child));
459 459 }
460 460 } else {
461 461 return Ok(None);
462 462 }
463 463 }
464 464 }
465 465
466 466 /// Returns a mutable reference to the node at `path` if it exists
467 467 ///
468 468 /// This takes `root` instead of `&mut self` so that callers can mutate
469 469 /// other fields while the returned borrow is still valid
470 470 fn get_node_mut<'tree>(
471 471 on_disk: &'on_disk [u8],
472 472 root: &'tree mut ChildNodes<'on_disk>,
473 473 path: &HgPath,
474 474 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
475 475 let mut children = root;
476 476 let mut components = path.components();
477 477 let mut component =
478 478 components.next().expect("expected at least one components");
479 479 loop {
480 480 if let Some(child) = children.make_mut(on_disk)?.get_mut(component)
481 481 {
482 482 if let Some(next_component) = components.next() {
483 483 component = next_component;
484 484 children = &mut child.children;
485 485 } else {
486 486 return Ok(Some(child));
487 487 }
488 488 } else {
489 489 return Ok(None);
490 490 }
491 491 }
492 492 }
493 493
494 494 pub(super) fn get_or_insert_node<'tree, 'path>(
495 495 on_disk: &'on_disk [u8],
496 496 root: &'tree mut ChildNodes<'on_disk>,
497 497 path: &'path HgPath,
498 498 to_cow: impl Fn(
499 499 WithBasename<&'path HgPath>,
500 500 ) -> WithBasename<Cow<'on_disk, HgPath>>,
501 501 mut each_ancestor: impl FnMut(&mut Node),
502 502 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
503 503 let mut child_nodes = root;
504 504 let mut inclusive_ancestor_paths =
505 505 WithBasename::inclusive_ancestors_of(path);
506 506 let mut ancestor_path = inclusive_ancestor_paths
507 507 .next()
508 508 .expect("expected at least one inclusive ancestor");
509 509 loop {
510 510 // TODO: can we avoid allocating an owned key in cases where the
511 511 // map already contains that key, without introducing double
512 512 // lookup?
513 513 let child_node = child_nodes
514 514 .make_mut(on_disk)?
515 515 .entry(to_cow(ancestor_path))
516 516 .or_default();
517 517 if let Some(next) = inclusive_ancestor_paths.next() {
518 518 each_ancestor(child_node);
519 519 ancestor_path = next;
520 520 child_nodes = &mut child_node.children;
521 521 } else {
522 522 return Ok(child_node);
523 523 }
524 524 }
525 525 }
526 526
527 527 fn add_or_remove_file(
528 528 &mut self,
529 529 path: &HgPath,
530 530 old_state: EntryState,
531 531 new_entry: DirstateEntry,
532 532 ) -> Result<(), DirstateV2ParseError> {
533 533 let tracked_count_increment =
534 534 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
535 535 (false, true) => 1,
536 536 (true, false) => -1,
537 537 _ => 0,
538 538 };
539 539
540 540 let node = Self::get_or_insert_node(
541 541 self.on_disk,
542 542 &mut self.root,
543 543 path,
544 544 WithBasename::to_cow_owned,
545 545 |ancestor| {
546 546 // We can’t use `+= increment` because the counter is unsigned,
547 547 // and we want debug builds to detect accidental underflow
548 548 // through zero
549 549 match tracked_count_increment {
550 550 1 => ancestor.tracked_descendants_count += 1,
551 551 -1 => ancestor.tracked_descendants_count -= 1,
552 552 _ => {}
553 553 }
554 554 },
555 555 )?;
556 556 if !node.data.has_entry() {
557 557 self.nodes_with_entry_count += 1
558 558 }
559 559 node.data = NodeData::Entry(new_entry);
560 560 Ok(())
561 561 }
562 562
563 563 fn iter_nodes<'tree>(
564 564 &'tree self,
565 565 ) -> impl Iterator<
566 566 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
567 567 > + 'tree {
568 568 // Depth first tree traversal.
569 569 //
570 570 // If we could afford internal iteration and recursion,
571 571 // this would look like:
572 572 //
573 573 // ```
574 574 // fn traverse_children(
575 575 // children: &ChildNodes,
576 576 // each: &mut impl FnMut(&Node),
577 577 // ) {
578 578 // for child in children.values() {
579 579 // traverse_children(&child.children, each);
580 580 // each(child);
581 581 // }
582 582 // }
583 583 // ```
584 584 //
585 585 // However we want an external iterator and therefore can’t use the
586 586 // call stack. Use an explicit stack instead:
587 587 let mut stack = Vec::new();
588 588 let mut iter = self.root.as_ref().iter();
589 589 std::iter::from_fn(move || {
590 590 while let Some(child_node) = iter.next() {
591 591 let children = match child_node.children(self.on_disk) {
592 592 Ok(children) => children,
593 593 Err(error) => return Some(Err(error)),
594 594 };
595 595 // Pseudo-recursion
596 596 let new_iter = children.iter();
597 597 let old_iter = std::mem::replace(&mut iter, new_iter);
598 598 stack.push((child_node, old_iter));
599 599 }
600 600 // Found the end of a `children.iter()` iterator.
601 601 if let Some((child_node, next_iter)) = stack.pop() {
602 602 // "Return" from pseudo-recursion by restoring state from the
603 603 // explicit stack
604 604 iter = next_iter;
605 605
606 606 Some(Ok(child_node))
607 607 } else {
608 608 // Reached the bottom of the stack, we’re done
609 609 None
610 610 }
611 611 })
612 612 }
613 613
614 614 fn clear_known_ambiguous_mtimes(
615 615 &mut self,
616 616 paths: &[impl AsRef<HgPath>],
617 617 ) -> Result<(), DirstateV2ParseError> {
618 618 for path in paths {
619 619 if let Some(node) = Self::get_node_mut(
620 620 self.on_disk,
621 621 &mut self.root,
622 622 path.as_ref(),
623 623 )? {
624 624 if let NodeData::Entry(entry) = &mut node.data {
625 625 entry.clear_mtime();
626 626 }
627 627 }
628 628 }
629 629 Ok(())
630 630 }
631 631
632 632 /// Return a faillilble iterator of full paths of nodes that have an
633 633 /// `entry` for which the given `predicate` returns true.
634 634 ///
635 635 /// Fallibility means that each iterator item is a `Result`, which may
636 636 /// indicate a parse error of the on-disk dirstate-v2 format. Such errors
637 637 /// should only happen if Mercurial is buggy or a repository is corrupted.
638 638 fn filter_full_paths<'tree>(
639 639 &'tree self,
640 640 predicate: impl Fn(&DirstateEntry) -> bool + 'tree,
641 641 ) -> impl Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + 'tree
642 642 {
643 643 filter_map_results(self.iter_nodes(), move |node| {
644 644 if let Some(entry) = node.entry()? {
645 645 if predicate(&entry) {
646 646 return Ok(Some(node.full_path(self.on_disk)?));
647 647 }
648 648 }
649 649 Ok(None)
650 650 })
651 651 }
652 652 }
653 653
654 654 /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s.
655 655 ///
656 656 /// The callback is only called for incoming `Ok` values. Errors are passed
657 657 /// through as-is. In order to let it use the `?` operator the callback is
658 658 /// expected to return a `Result` of `Option`, instead of an `Option` of
659 659 /// `Result`.
660 660 fn filter_map_results<'a, I, F, A, B, E>(
661 661 iter: I,
662 662 f: F,
663 663 ) -> impl Iterator<Item = Result<B, E>> + 'a
664 664 where
665 665 I: Iterator<Item = Result<A, E>> + 'a,
666 666 F: Fn(A) -> Result<Option<B>, E> + 'a,
667 667 {
668 668 iter.filter_map(move |result| match result {
669 669 Ok(node) => f(node).transpose(),
670 670 Err(e) => Some(Err(e)),
671 671 })
672 672 }
673 673
674 674 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
675 675 fn clear(&mut self) {
676 676 self.root = Default::default();
677 677 self.nodes_with_entry_count = 0;
678 678 self.nodes_with_copy_source_count = 0;
679 679 }
680 680
681 681 fn add_file(
682 682 &mut self,
683 683 filename: &HgPath,
684 684 old_state: EntryState,
685 685 entry: DirstateEntry,
686 686 ) -> Result<(), DirstateError> {
687 687 Ok(self.add_or_remove_file(filename, old_state, entry)?)
688 688 }
689 689
690 690 fn remove_file(
691 691 &mut self,
692 692 filename: &HgPath,
693 693 old_state: EntryState,
694 694 size: i32,
695 695 ) -> Result<(), DirstateError> {
696 696 let entry = DirstateEntry {
697 697 state: EntryState::Removed,
698 698 mode: 0,
699 699 size,
700 700 mtime: 0,
701 701 };
702 702 Ok(self.add_or_remove_file(filename, old_state, entry)?)
703 703 }
704 704
705 705 fn drop_file(
706 706 &mut self,
707 707 filename: &HgPath,
708 708 old_state: EntryState,
709 709 ) -> Result<bool, DirstateError> {
710 710 struct Dropped {
711 711 was_tracked: bool,
712 712 had_entry: bool,
713 713 had_copy_source: bool,
714 714 }
715
716 /// If this returns `Ok(Some((dropped, removed)))`, then
717 ///
718 /// * `dropped` is about the leaf node that was at `filename`
719 /// * `removed` is whether this particular level of recursion just
720 /// removed a node in `nodes`.
715 721 fn recur<'on_disk>(
716 722 on_disk: &'on_disk [u8],
717 723 nodes: &mut ChildNodes<'on_disk>,
718 724 path: &HgPath,
719 ) -> Result<Option<Dropped>, DirstateV2ParseError> {
725 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
720 726 let (first_path_component, rest_of_path) =
721 727 path.split_first_component();
722 728 let node = if let Some(node) =
723 729 nodes.make_mut(on_disk)?.get_mut(first_path_component)
724 730 {
725 731 node
726 732 } else {
727 733 return Ok(None);
728 734 };
729 735 let dropped;
730 736 if let Some(rest) = rest_of_path {
731 if let Some(d) = recur(on_disk, &mut node.children, rest)? {
737 if let Some((d, removed)) =
738 recur(on_disk, &mut node.children, rest)?
739 {
732 740 dropped = d;
733 741 if dropped.was_tracked {
734 742 node.tracked_descendants_count -= 1;
735 743 }
744
745 // Directory caches must be invalidated when removing a
746 // child node
747 if removed {
748 if let NodeData::CachedDirectory { .. } = &node.data {
749 node.data = NodeData::None
750 }
751 }
736 752 } else {
737 753 return Ok(None);
738 754 }
739 755 } else {
740 756 let had_entry = node.data.has_entry();
741 757 if had_entry {
742 758 node.data = NodeData::None
743 759 }
744 760 dropped = Dropped {
745 761 was_tracked: node
746 762 .data
747 763 .as_entry()
748 764 .map_or(false, |entry| entry.state.is_tracked()),
749 765 had_entry,
750 766 had_copy_source: node.copy_source.take().is_some(),
751 767 };
752 768 }
753 769 // After recursion, for both leaf (rest_of_path is None) nodes and
754 770 // parent nodes, remove a node if it just became empty.
755 if !node.data.has_entry()
771 let remove = !node.data.has_entry()
756 772 && node.copy_source.is_none()
757 && node.children.is_empty()
758 {
773 && node.children.is_empty();
774 if remove {
759 775 nodes.make_mut(on_disk)?.remove(first_path_component);
760 776 }
761 Ok(Some(dropped))
777 Ok(Some((dropped, remove)))
762 778 }
763 779
764 if let Some(dropped) = recur(self.on_disk, &mut self.root, filename)? {
780 if let Some((dropped, _removed)) =
781 recur(self.on_disk, &mut self.root, filename)?
782 {
765 783 if dropped.had_entry {
766 784 self.nodes_with_entry_count -= 1
767 785 }
768 786 if dropped.had_copy_source {
769 787 self.nodes_with_copy_source_count -= 1
770 788 }
771 789 Ok(dropped.had_entry)
772 790 } else {
773 791 debug_assert!(!old_state.is_tracked());
774 792 Ok(false)
775 793 }
776 794 }
777 795
778 796 fn clear_ambiguous_times(
779 797 &mut self,
780 798 filenames: Vec<HgPathBuf>,
781 799 now: i32,
782 800 ) -> Result<(), DirstateV2ParseError> {
783 801 for filename in filenames {
784 802 if let Some(node) =
785 803 Self::get_node_mut(self.on_disk, &mut self.root, &filename)?
786 804 {
787 805 if let NodeData::Entry(entry) = &mut node.data {
788 806 entry.clear_ambiguous_mtime(now);
789 807 }
790 808 }
791 809 }
792 810 Ok(())
793 811 }
794 812
795 813 fn non_normal_entries_contains(
796 814 &mut self,
797 815 key: &HgPath,
798 816 ) -> Result<bool, DirstateV2ParseError> {
799 817 Ok(if let Some(node) = self.get_node(key)? {
800 818 node.entry()?.map_or(false, |entry| entry.is_non_normal())
801 819 } else {
802 820 false
803 821 })
804 822 }
805 823
806 824 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
807 825 // Do nothing, this `DirstateMap` does not have a separate "non normal
808 826 // entries" set that need to be kept up to date
809 827 }
810 828
811 829 fn non_normal_or_other_parent_paths(
812 830 &mut self,
813 831 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
814 832 {
815 833 Box::new(self.filter_full_paths(|entry| {
816 834 entry.is_non_normal() || entry.is_from_other_parent()
817 835 }))
818 836 }
819 837
820 838 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
821 839 // Do nothing, this `DirstateMap` does not have a separate "non normal
822 840 // entries" and "from other parent" sets that need to be recomputed
823 841 }
824 842
825 843 fn iter_non_normal_paths(
826 844 &mut self,
827 845 ) -> Box<
828 846 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
829 847 > {
830 848 self.iter_non_normal_paths_panic()
831 849 }
832 850
833 851 fn iter_non_normal_paths_panic(
834 852 &self,
835 853 ) -> Box<
836 854 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
837 855 > {
838 856 Box::new(self.filter_full_paths(|entry| entry.is_non_normal()))
839 857 }
840 858
841 859 fn iter_other_parent_paths(
842 860 &mut self,
843 861 ) -> Box<
844 862 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
845 863 > {
846 864 Box::new(self.filter_full_paths(|entry| entry.is_from_other_parent()))
847 865 }
848 866
849 867 fn has_tracked_dir(
850 868 &mut self,
851 869 directory: &HgPath,
852 870 ) -> Result<bool, DirstateError> {
853 871 if let Some(node) = self.get_node(directory)? {
854 872 // A node without a `DirstateEntry` was created to hold child
855 873 // nodes, and is therefore a directory.
856 874 let state = node.state()?;
857 875 Ok(state.is_none() && node.tracked_descendants_count() > 0)
858 876 } else {
859 877 Ok(false)
860 878 }
861 879 }
862 880
863 881 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
864 882 if let Some(node) = self.get_node(directory)? {
865 883 // A node without a `DirstateEntry` was created to hold child
866 884 // nodes, and is therefore a directory.
867 885 Ok(node.state()?.is_none())
868 886 } else {
869 887 Ok(false)
870 888 }
871 889 }
872 890
873 891 #[timed]
874 892 fn pack_v1(
875 893 &mut self,
876 894 parents: DirstateParents,
877 895 now: Timestamp,
878 896 ) -> Result<Vec<u8>, DirstateError> {
879 897 let now: i32 = now.0.try_into().expect("time overflow");
880 898 let mut ambiguous_mtimes = Vec::new();
881 899 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
882 900 // reallocations
883 901 let mut size = parents.as_bytes().len();
884 902 for node in self.iter_nodes() {
885 903 let node = node?;
886 904 if let Some(entry) = node.entry()? {
887 905 size += packed_entry_size(
888 906 node.full_path(self.on_disk)?,
889 907 node.copy_source(self.on_disk)?,
890 908 );
891 909 if entry.mtime_is_ambiguous(now) {
892 910 ambiguous_mtimes.push(
893 911 node.full_path_borrowed(self.on_disk)?
894 912 .detach_from_tree(),
895 913 )
896 914 }
897 915 }
898 916 }
899 917 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes)?;
900 918
901 919 let mut packed = Vec::with_capacity(size);
902 920 packed.extend(parents.as_bytes());
903 921
904 922 for node in self.iter_nodes() {
905 923 let node = node?;
906 924 if let Some(entry) = node.entry()? {
907 925 pack_entry(
908 926 node.full_path(self.on_disk)?,
909 927 &entry,
910 928 node.copy_source(self.on_disk)?,
911 929 &mut packed,
912 930 );
913 931 }
914 932 }
915 933 Ok(packed)
916 934 }
917 935
918 936 #[timed]
919 937 fn pack_v2(
920 938 &mut self,
921 939 parents: DirstateParents,
922 940 now: Timestamp,
923 941 ) -> Result<Vec<u8>, DirstateError> {
924 942 // TODO: how do we want to handle this in 2038?
925 943 let now: i32 = now.0.try_into().expect("time overflow");
926 944 let mut paths = Vec::new();
927 945 for node in self.iter_nodes() {
928 946 let node = node?;
929 947 if let Some(entry) = node.entry()? {
930 948 if entry.mtime_is_ambiguous(now) {
931 949 paths.push(
932 950 node.full_path_borrowed(self.on_disk)?
933 951 .detach_from_tree(),
934 952 )
935 953 }
936 954 }
937 955 }
938 956 // Borrow of `self` ends here since we collect cloned paths
939 957
940 958 self.clear_known_ambiguous_mtimes(&paths)?;
941 959
942 960 on_disk::write(self, parents)
943 961 }
944 962
945 963 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
946 964 // Do nothing, this `DirstateMap` does not a separate `all_dirs` that
947 965 // needs to be recomputed
948 966 Ok(())
949 967 }
950 968
951 969 fn set_dirs(&mut self) -> Result<(), DirstateError> {
952 970 // Do nothing, this `DirstateMap` does not a separate `dirs` that needs
953 971 // to be recomputed
954 972 Ok(())
955 973 }
956 974
957 975 fn status<'a>(
958 976 &'a mut self,
959 977 matcher: &'a (dyn Matcher + Sync),
960 978 root_dir: PathBuf,
961 979 ignore_files: Vec<PathBuf>,
962 980 options: StatusOptions,
963 981 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
964 982 {
965 983 super::status::status(self, matcher, root_dir, ignore_files, options)
966 984 }
967 985
968 986 fn copy_map_len(&self) -> usize {
969 987 self.nodes_with_copy_source_count as usize
970 988 }
971 989
972 990 fn copy_map_iter(&self) -> CopyMapIter<'_> {
973 991 Box::new(filter_map_results(self.iter_nodes(), move |node| {
974 992 Ok(if let Some(source) = node.copy_source(self.on_disk)? {
975 993 Some((node.full_path(self.on_disk)?, source))
976 994 } else {
977 995 None
978 996 })
979 997 }))
980 998 }
981 999
982 1000 fn copy_map_contains_key(
983 1001 &self,
984 1002 key: &HgPath,
985 1003 ) -> Result<bool, DirstateV2ParseError> {
986 1004 Ok(if let Some(node) = self.get_node(key)? {
987 1005 node.has_copy_source()
988 1006 } else {
989 1007 false
990 1008 })
991 1009 }
992 1010
993 1011 fn copy_map_get(
994 1012 &self,
995 1013 key: &HgPath,
996 1014 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
997 1015 if let Some(node) = self.get_node(key)? {
998 1016 if let Some(source) = node.copy_source(self.on_disk)? {
999 1017 return Ok(Some(source));
1000 1018 }
1001 1019 }
1002 1020 Ok(None)
1003 1021 }
1004 1022
1005 1023 fn copy_map_remove(
1006 1024 &mut self,
1007 1025 key: &HgPath,
1008 1026 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1009 1027 let count = &mut self.nodes_with_copy_source_count;
1010 1028 Ok(
1011 1029 Self::get_node_mut(self.on_disk, &mut self.root, key)?.and_then(
1012 1030 |node| {
1013 1031 if node.copy_source.is_some() {
1014 1032 *count -= 1
1015 1033 }
1016 1034 node.copy_source.take().map(Cow::into_owned)
1017 1035 },
1018 1036 ),
1019 1037 )
1020 1038 }
1021 1039
1022 1040 fn copy_map_insert(
1023 1041 &mut self,
1024 1042 key: HgPathBuf,
1025 1043 value: HgPathBuf,
1026 1044 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1027 1045 let node = Self::get_or_insert_node(
1028 1046 self.on_disk,
1029 1047 &mut self.root,
1030 1048 &key,
1031 1049 WithBasename::to_cow_owned,
1032 1050 |_ancestor| {},
1033 1051 )?;
1034 1052 if node.copy_source.is_none() {
1035 1053 self.nodes_with_copy_source_count += 1
1036 1054 }
1037 1055 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1038 1056 }
1039 1057
1040 1058 fn len(&self) -> usize {
1041 1059 self.nodes_with_entry_count as usize
1042 1060 }
1043 1061
1044 1062 fn contains_key(
1045 1063 &self,
1046 1064 key: &HgPath,
1047 1065 ) -> Result<bool, DirstateV2ParseError> {
1048 1066 Ok(self.get(key)?.is_some())
1049 1067 }
1050 1068
1051 1069 fn get(
1052 1070 &self,
1053 1071 key: &HgPath,
1054 1072 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1055 1073 Ok(if let Some(node) = self.get_node(key)? {
1056 1074 node.entry()?
1057 1075 } else {
1058 1076 None
1059 1077 })
1060 1078 }
1061 1079
1062 1080 fn iter(&self) -> StateMapIter<'_> {
1063 1081 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1064 1082 Ok(if let Some(entry) = node.entry()? {
1065 1083 Some((node.full_path(self.on_disk)?, entry))
1066 1084 } else {
1067 1085 None
1068 1086 })
1069 1087 }))
1070 1088 }
1071 1089
1072 1090 fn iter_directories(
1073 1091 &self,
1074 1092 ) -> Box<
1075 1093 dyn Iterator<
1076 1094 Item = Result<
1077 1095 (&HgPath, Option<Timestamp>),
1078 1096 DirstateV2ParseError,
1079 1097 >,
1080 1098 > + Send
1081 1099 + '_,
1082 1100 > {
1083 1101 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1084 1102 Ok(if node.state()?.is_none() {
1085 1103 Some((
1086 1104 node.full_path(self.on_disk)?,
1087 1105 node.cached_directory_mtime()
1088 1106 .map(|mtime| Timestamp(mtime.seconds())),
1089 1107 ))
1090 1108 } else {
1091 1109 None
1092 1110 })
1093 1111 }))
1094 1112 }
1095 1113 }
@@ -1,960 +1,974 b''
1 1 #testcases dirstate-v1 dirstate-v1-tree dirstate-v2
2 2
3 3 #if no-rust
4 4 $ hg init repo0 --config format.exp-dirstate-v2=1
5 5 abort: dirstate v2 format requested by config but not supported (requires Rust extensions)
6 6 [255]
7 7 #endif
8 8
9 9 #if dirstate-v1-tree
10 10 #require rust
11 11 $ echo '[experimental]' >> $HGRCPATH
12 12 $ echo 'dirstate-tree.in-memory=1' >> $HGRCPATH
13 13 #endif
14 14
15 15 #if dirstate-v2
16 16 #require rust
17 17 $ echo '[format]' >> $HGRCPATH
18 18 $ echo 'exp-dirstate-v2=1' >> $HGRCPATH
19 19 #endif
20 20
21 21 $ hg init repo1
22 22 $ cd repo1
23 23 $ mkdir a b a/1 b/1 b/2
24 24 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
25 25
26 26 hg status in repo root:
27 27
28 28 $ hg status
29 29 ? a/1/in_a_1
30 30 ? a/in_a
31 31 ? b/1/in_b_1
32 32 ? b/2/in_b_2
33 33 ? b/in_b
34 34 ? in_root
35 35
36 36 hg status . in repo root:
37 37
38 38 $ hg status .
39 39 ? a/1/in_a_1
40 40 ? a/in_a
41 41 ? b/1/in_b_1
42 42 ? b/2/in_b_2
43 43 ? b/in_b
44 44 ? in_root
45 45
46 46 $ hg status --cwd a
47 47 ? a/1/in_a_1
48 48 ? a/in_a
49 49 ? b/1/in_b_1
50 50 ? b/2/in_b_2
51 51 ? b/in_b
52 52 ? in_root
53 53 $ hg status --cwd a .
54 54 ? 1/in_a_1
55 55 ? in_a
56 56 $ hg status --cwd a ..
57 57 ? 1/in_a_1
58 58 ? in_a
59 59 ? ../b/1/in_b_1
60 60 ? ../b/2/in_b_2
61 61 ? ../b/in_b
62 62 ? ../in_root
63 63
64 64 $ hg status --cwd b
65 65 ? a/1/in_a_1
66 66 ? a/in_a
67 67 ? b/1/in_b_1
68 68 ? b/2/in_b_2
69 69 ? b/in_b
70 70 ? in_root
71 71 $ hg status --cwd b .
72 72 ? 1/in_b_1
73 73 ? 2/in_b_2
74 74 ? in_b
75 75 $ hg status --cwd b ..
76 76 ? ../a/1/in_a_1
77 77 ? ../a/in_a
78 78 ? 1/in_b_1
79 79 ? 2/in_b_2
80 80 ? in_b
81 81 ? ../in_root
82 82
83 83 $ hg status --cwd a/1
84 84 ? a/1/in_a_1
85 85 ? a/in_a
86 86 ? b/1/in_b_1
87 87 ? b/2/in_b_2
88 88 ? b/in_b
89 89 ? in_root
90 90 $ hg status --cwd a/1 .
91 91 ? in_a_1
92 92 $ hg status --cwd a/1 ..
93 93 ? in_a_1
94 94 ? ../in_a
95 95
96 96 $ hg status --cwd b/1
97 97 ? a/1/in_a_1
98 98 ? a/in_a
99 99 ? b/1/in_b_1
100 100 ? b/2/in_b_2
101 101 ? b/in_b
102 102 ? in_root
103 103 $ hg status --cwd b/1 .
104 104 ? in_b_1
105 105 $ hg status --cwd b/1 ..
106 106 ? in_b_1
107 107 ? ../2/in_b_2
108 108 ? ../in_b
109 109
110 110 $ hg status --cwd b/2
111 111 ? a/1/in_a_1
112 112 ? a/in_a
113 113 ? b/1/in_b_1
114 114 ? b/2/in_b_2
115 115 ? b/in_b
116 116 ? in_root
117 117 $ hg status --cwd b/2 .
118 118 ? in_b_2
119 119 $ hg status --cwd b/2 ..
120 120 ? ../1/in_b_1
121 121 ? in_b_2
122 122 ? ../in_b
123 123
124 124 combining patterns with root and patterns without a root works
125 125
126 126 $ hg st a/in_a re:.*b$
127 127 ? a/in_a
128 128 ? b/in_b
129 129
130 130 tweaking defaults works
131 131 $ hg status --cwd a --config ui.tweakdefaults=yes
132 132 ? 1/in_a_1
133 133 ? in_a
134 134 ? ../b/1/in_b_1
135 135 ? ../b/2/in_b_2
136 136 ? ../b/in_b
137 137 ? ../in_root
138 138 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
139 139 ? a/1/in_a_1 (glob)
140 140 ? a/in_a (glob)
141 141 ? b/1/in_b_1 (glob)
142 142 ? b/2/in_b_2 (glob)
143 143 ? b/in_b (glob)
144 144 ? in_root
145 145 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
146 146 ? 1/in_a_1
147 147 ? in_a
148 148 ? ../b/1/in_b_1
149 149 ? ../b/2/in_b_2
150 150 ? ../b/in_b
151 151 ? ../in_root (glob)
152 152
153 153 relative paths can be requested
154 154
155 155 $ hg status --cwd a --config ui.relative-paths=yes
156 156 ? 1/in_a_1
157 157 ? in_a
158 158 ? ../b/1/in_b_1
159 159 ? ../b/2/in_b_2
160 160 ? ../b/in_b
161 161 ? ../in_root
162 162
163 163 $ hg status --cwd a . --config ui.relative-paths=legacy
164 164 ? 1/in_a_1
165 165 ? in_a
166 166 $ hg status --cwd a . --config ui.relative-paths=no
167 167 ? a/1/in_a_1
168 168 ? a/in_a
169 169
170 170 commands.status.relative overrides ui.relative-paths
171 171
172 172 $ cat >> $HGRCPATH <<EOF
173 173 > [ui]
174 174 > relative-paths = False
175 175 > [commands]
176 176 > status.relative = True
177 177 > EOF
178 178 $ hg status --cwd a
179 179 ? 1/in_a_1
180 180 ? in_a
181 181 ? ../b/1/in_b_1
182 182 ? ../b/2/in_b_2
183 183 ? ../b/in_b
184 184 ? ../in_root
185 185 $ HGPLAIN=1 hg status --cwd a
186 186 ? a/1/in_a_1 (glob)
187 187 ? a/in_a (glob)
188 188 ? b/1/in_b_1 (glob)
189 189 ? b/2/in_b_2 (glob)
190 190 ? b/in_b (glob)
191 191 ? in_root
192 192
193 193 if relative paths are explicitly off, tweakdefaults doesn't change it
194 194 $ cat >> $HGRCPATH <<EOF
195 195 > [commands]
196 196 > status.relative = False
197 197 > EOF
198 198 $ hg status --cwd a --config ui.tweakdefaults=yes
199 199 ? a/1/in_a_1
200 200 ? a/in_a
201 201 ? b/1/in_b_1
202 202 ? b/2/in_b_2
203 203 ? b/in_b
204 204 ? in_root
205 205
206 206 $ cd ..
207 207
208 208 $ hg init repo2
209 209 $ cd repo2
210 210 $ touch modified removed deleted ignored
211 211 $ echo "^ignored$" > .hgignore
212 212 $ hg ci -A -m 'initial checkin'
213 213 adding .hgignore
214 214 adding deleted
215 215 adding modified
216 216 adding removed
217 217 $ touch modified added unknown ignored
218 218 $ hg add added
219 219 $ hg remove removed
220 220 $ rm deleted
221 221
222 222 hg status:
223 223
224 224 $ hg status
225 225 A added
226 226 R removed
227 227 ! deleted
228 228 ? unknown
229 229
230 230 hg status modified added removed deleted unknown never-existed ignored:
231 231
232 232 $ hg status modified added removed deleted unknown never-existed ignored
233 233 never-existed: * (glob)
234 234 A added
235 235 R removed
236 236 ! deleted
237 237 ? unknown
238 238
239 239 $ hg copy modified copied
240 240
241 241 hg status -C:
242 242
243 243 $ hg status -C
244 244 A added
245 245 A copied
246 246 modified
247 247 R removed
248 248 ! deleted
249 249 ? unknown
250 250
251 251 hg status -A:
252 252
253 253 $ hg status -A
254 254 A added
255 255 A copied
256 256 modified
257 257 R removed
258 258 ! deleted
259 259 ? unknown
260 260 I ignored
261 261 C .hgignore
262 262 C modified
263 263
264 264 $ hg status -A -T '{status} {path} {node|shortest}\n'
265 265 A added ffff
266 266 A copied ffff
267 267 R removed ffff
268 268 ! deleted ffff
269 269 ? unknown ffff
270 270 I ignored ffff
271 271 C .hgignore ffff
272 272 C modified ffff
273 273
274 274 $ hg status -A -Tjson
275 275 [
276 276 {
277 277 "itemtype": "file",
278 278 "path": "added",
279 279 "status": "A"
280 280 },
281 281 {
282 282 "itemtype": "file",
283 283 "path": "copied",
284 284 "source": "modified",
285 285 "status": "A"
286 286 },
287 287 {
288 288 "itemtype": "file",
289 289 "path": "removed",
290 290 "status": "R"
291 291 },
292 292 {
293 293 "itemtype": "file",
294 294 "path": "deleted",
295 295 "status": "!"
296 296 },
297 297 {
298 298 "itemtype": "file",
299 299 "path": "unknown",
300 300 "status": "?"
301 301 },
302 302 {
303 303 "itemtype": "file",
304 304 "path": "ignored",
305 305 "status": "I"
306 306 },
307 307 {
308 308 "itemtype": "file",
309 309 "path": ".hgignore",
310 310 "status": "C"
311 311 },
312 312 {
313 313 "itemtype": "file",
314 314 "path": "modified",
315 315 "status": "C"
316 316 }
317 317 ]
318 318
319 319 $ hg status -A -Tpickle > pickle
320 320 >>> from __future__ import print_function
321 321 >>> from mercurial import util
322 322 >>> pickle = util.pickle
323 323 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
324 324 >>> for s, p in data: print("%s %s" % (s, p))
325 325 ! deleted
326 326 ? pickle
327 327 ? unknown
328 328 A added
329 329 A copied
330 330 C .hgignore
331 331 C modified
332 332 I ignored
333 333 R removed
334 334 $ rm pickle
335 335
336 336 $ echo "^ignoreddir$" > .hgignore
337 337 $ mkdir ignoreddir
338 338 $ touch ignoreddir/file
339 339
340 340 Test templater support:
341 341
342 342 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
343 343 [M] .hgignore
344 344 [A] added
345 345 [A] modified -> copied
346 346 [R] removed
347 347 [!] deleted
348 348 [?] ignored
349 349 [?] unknown
350 350 [I] ignoreddir/file
351 351 [C] modified
352 352 $ hg status -AT default
353 353 M .hgignore
354 354 A added
355 355 A copied
356 356 modified
357 357 R removed
358 358 ! deleted
359 359 ? ignored
360 360 ? unknown
361 361 I ignoreddir/file
362 362 C modified
363 363 $ hg status -T compact
364 364 abort: "status" not in template map
365 365 [255]
366 366
367 367 hg status ignoreddir/file:
368 368
369 369 $ hg status ignoreddir/file
370 370
371 371 hg status -i ignoreddir/file:
372 372
373 373 $ hg status -i ignoreddir/file
374 374 I ignoreddir/file
375 375 $ cd ..
376 376
377 377 Check 'status -q' and some combinations
378 378
379 379 $ hg init repo3
380 380 $ cd repo3
381 381 $ touch modified removed deleted ignored
382 382 $ echo "^ignored$" > .hgignore
383 383 $ hg commit -A -m 'initial checkin'
384 384 adding .hgignore
385 385 adding deleted
386 386 adding modified
387 387 adding removed
388 388 $ touch added unknown ignored
389 389 $ hg add added
390 390 $ echo "test" >> modified
391 391 $ hg remove removed
392 392 $ rm deleted
393 393 $ hg copy modified copied
394 394
395 395 Specify working directory revision explicitly, that should be the same as
396 396 "hg status"
397 397
398 398 $ hg status --change "wdir()"
399 399 M modified
400 400 A added
401 401 A copied
402 402 R removed
403 403 ! deleted
404 404 ? unknown
405 405
406 406 Run status with 2 different flags.
407 407 Check if result is the same or different.
408 408 If result is not as expected, raise error
409 409
410 410 $ assert() {
411 411 > hg status $1 > ../a
412 412 > hg status $2 > ../b
413 413 > if diff ../a ../b > /dev/null; then
414 414 > out=0
415 415 > else
416 416 > out=1
417 417 > fi
418 418 > if [ $3 -eq 0 ]; then
419 419 > df="same"
420 420 > else
421 421 > df="different"
422 422 > fi
423 423 > if [ $out -ne $3 ]; then
424 424 > echo "Error on $1 and $2, should be $df."
425 425 > fi
426 426 > }
427 427
428 428 Assert flag1 flag2 [0-same | 1-different]
429 429
430 430 $ assert "-q" "-mard" 0
431 431 $ assert "-A" "-marduicC" 0
432 432 $ assert "-qA" "-mardcC" 0
433 433 $ assert "-qAui" "-A" 0
434 434 $ assert "-qAu" "-marducC" 0
435 435 $ assert "-qAi" "-mardicC" 0
436 436 $ assert "-qu" "-u" 0
437 437 $ assert "-q" "-u" 1
438 438 $ assert "-m" "-a" 1
439 439 $ assert "-r" "-d" 1
440 440 $ cd ..
441 441
442 442 $ hg init repo4
443 443 $ cd repo4
444 444 $ touch modified removed deleted
445 445 $ hg ci -q -A -m 'initial checkin'
446 446 $ touch added unknown
447 447 $ hg add added
448 448 $ hg remove removed
449 449 $ rm deleted
450 450 $ echo x > modified
451 451 $ hg copy modified copied
452 452 $ hg ci -m 'test checkin' -d "1000001 0"
453 453 $ rm *
454 454 $ touch unrelated
455 455 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
456 456
457 457 hg status --change 1:
458 458
459 459 $ hg status --change 1
460 460 M modified
461 461 A added
462 462 A copied
463 463 R removed
464 464
465 465 hg status --change 1 unrelated:
466 466
467 467 $ hg status --change 1 unrelated
468 468
469 469 hg status -C --change 1 added modified copied removed deleted:
470 470
471 471 $ hg status -C --change 1 added modified copied removed deleted
472 472 M modified
473 473 A added
474 474 A copied
475 475 modified
476 476 R removed
477 477
478 478 hg status -A --change 1 and revset:
479 479
480 480 $ hg status -A --change '1|1'
481 481 M modified
482 482 A added
483 483 A copied
484 484 modified
485 485 R removed
486 486 C deleted
487 487
488 488 $ cd ..
489 489
490 490 hg status with --rev and reverted changes:
491 491
492 492 $ hg init reverted-changes-repo
493 493 $ cd reverted-changes-repo
494 494 $ echo a > file
495 495 $ hg add file
496 496 $ hg ci -m a
497 497 $ echo b > file
498 498 $ hg ci -m b
499 499
500 500 reverted file should appear clean
501 501
502 502 $ hg revert -r 0 .
503 503 reverting file
504 504 $ hg status -A --rev 0
505 505 C file
506 506
507 507 #if execbit
508 508 reverted file with changed flag should appear modified
509 509
510 510 $ chmod +x file
511 511 $ hg status -A --rev 0
512 512 M file
513 513
514 514 $ hg revert -r 0 .
515 515 reverting file
516 516
517 517 reverted and committed file with changed flag should appear modified
518 518
519 519 $ hg co -C .
520 520 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
521 521 $ chmod +x file
522 522 $ hg ci -m 'change flag'
523 523 $ hg status -A --rev 1 --rev 2
524 524 M file
525 525 $ hg diff -r 1 -r 2
526 526
527 527 #endif
528 528
529 529 $ cd ..
530 530
531 531 hg status of binary file starting with '\1\n', a separator for metadata:
532 532
533 533 $ hg init repo5
534 534 $ cd repo5
535 535 >>> open("010a", r"wb").write(b"\1\nfoo") and None
536 536 $ hg ci -q -A -m 'initial checkin'
537 537 $ hg status -A
538 538 C 010a
539 539
540 540 >>> open("010a", r"wb").write(b"\1\nbar") and None
541 541 $ hg status -A
542 542 M 010a
543 543 $ hg ci -q -m 'modify 010a'
544 544 $ hg status -A --rev 0:1
545 545 M 010a
546 546
547 547 $ touch empty
548 548 $ hg ci -q -A -m 'add another file'
549 549 $ hg status -A --rev 1:2 010a
550 550 C 010a
551 551
552 552 $ cd ..
553 553
554 554 test "hg status" with "directory pattern" which matches against files
555 555 only known on target revision.
556 556
557 557 $ hg init repo6
558 558 $ cd repo6
559 559
560 560 $ echo a > a.txt
561 561 $ hg add a.txt
562 562 $ hg commit -m '#0'
563 563 $ mkdir -p 1/2/3/4/5
564 564 $ echo b > 1/2/3/4/5/b.txt
565 565 $ hg add 1/2/3/4/5/b.txt
566 566 $ hg commit -m '#1'
567 567
568 568 $ hg update -C 0 > /dev/null
569 569 $ hg status -A
570 570 C a.txt
571 571
572 572 the directory matching against specified pattern should be removed,
573 573 because directory existence prevents 'dirstate.walk()' from showing
574 574 warning message about such pattern.
575 575
576 576 $ test ! -d 1
577 577 $ hg status -A --rev 1 1/2/3/4/5/b.txt
578 578 R 1/2/3/4/5/b.txt
579 579 $ hg status -A --rev 1 1/2/3/4/5
580 580 R 1/2/3/4/5/b.txt
581 581 $ hg status -A --rev 1 1/2/3
582 582 R 1/2/3/4/5/b.txt
583 583 $ hg status -A --rev 1 1
584 584 R 1/2/3/4/5/b.txt
585 585
586 586 $ hg status --config ui.formatdebug=True --rev 1 1
587 587 status = [
588 588 {
589 589 'itemtype': 'file',
590 590 'path': '1/2/3/4/5/b.txt',
591 591 'status': 'R'
592 592 },
593 593 ]
594 594
595 595 #if windows
596 596 $ hg --config ui.slash=false status -A --rev 1 1
597 597 R 1\2\3\4\5\b.txt
598 598 #endif
599 599
600 600 $ cd ..
601 601
602 602 Status after move overwriting a file (issue4458)
603 603 =================================================
604 604
605 605
606 606 $ hg init issue4458
607 607 $ cd issue4458
608 608 $ echo a > a
609 609 $ echo b > b
610 610 $ hg commit -Am base
611 611 adding a
612 612 adding b
613 613
614 614
615 615 with --force
616 616
617 617 $ hg mv b --force a
618 618 $ hg st --copies
619 619 M a
620 620 b
621 621 R b
622 622 $ hg revert --all
623 623 reverting a
624 624 undeleting b
625 625 $ rm *.orig
626 626
627 627 without force
628 628
629 629 $ hg rm a
630 630 $ hg st --copies
631 631 R a
632 632 $ hg mv b a
633 633 $ hg st --copies
634 634 M a
635 635 b
636 636 R b
637 637
638 638 using ui.statuscopies setting
639 639 $ hg st --config ui.statuscopies=true
640 640 M a
641 641 b
642 642 R b
643 643 $ hg st --config ui.statuscopies=false
644 644 M a
645 645 R b
646 646 $ hg st --config ui.tweakdefaults=yes
647 647 M a
648 648 b
649 649 R b
650 650
651 651 using log status template (issue5155)
652 652 $ hg log -Tstatus -r 'wdir()' -C
653 653 changeset: 2147483647:ffffffffffff
654 654 parent: 0:8c55c58b4c0e
655 655 user: test
656 656 date: * (glob)
657 657 files:
658 658 M a
659 659 b
660 660 R b
661 661
662 662 $ hg log -GTstatus -r 'wdir()' -C
663 663 o changeset: 2147483647:ffffffffffff
664 664 | parent: 0:8c55c58b4c0e
665 665 ~ user: test
666 666 date: * (glob)
667 667 files:
668 668 M a
669 669 b
670 670 R b
671 671
672 672
673 673 Other "bug" highlight, the revision status does not report the copy information.
674 674 This is buggy behavior.
675 675
676 676 $ hg commit -m 'blah'
677 677 $ hg st --copies --change .
678 678 M a
679 679 R b
680 680
681 681 using log status template, the copy information is displayed correctly.
682 682 $ hg log -Tstatus -r. -C
683 683 changeset: 1:6685fde43d21
684 684 tag: tip
685 685 user: test
686 686 date: * (glob)
687 687 summary: blah
688 688 files:
689 689 M a
690 690 b
691 691 R b
692 692
693 693
694 694 $ cd ..
695 695
696 696 Make sure .hg doesn't show up even as a symlink
697 697
698 698 $ hg init repo0
699 699 $ mkdir symlink-repo0
700 700 $ cd symlink-repo0
701 701 $ ln -s ../repo0/.hg
702 702 $ hg status
703 703
704 704 If the size hasnt changed but mtime has, status needs to read the contents
705 705 of the file to check whether it has changed
706 706
707 707 $ echo 1 > a
708 708 $ echo 1 > b
709 709 $ touch -t 200102030000 a b
710 710 $ hg commit -Aqm '#0'
711 711 $ echo 2 > a
712 712 $ touch -t 200102040000 a b
713 713 $ hg status
714 714 M a
715 715
716 716 Asking specifically for the status of a deleted/removed file
717 717
718 718 $ rm a
719 719 $ rm b
720 720 $ hg status a
721 721 ! a
722 722 $ hg rm a
723 723 $ hg rm b
724 724 $ hg status a
725 725 R a
726 726 $ hg commit -qm '#1'
727 727 $ hg status a
728 728 a: $ENOENT$
729 729
730 730 Check using include flag with pattern when status does not need to traverse
731 731 the working directory (issue6483)
732 732
733 733 $ cd ..
734 734 $ hg init issue6483
735 735 $ cd issue6483
736 736 $ touch a.py b.rs
737 737 $ hg add a.py b.rs
738 738 $ hg st -aI "*.py"
739 739 A a.py
740 740
741 741 Also check exclude pattern
742 742
743 743 $ hg st -aX "*.rs"
744 744 A a.py
745 745
746 746 issue6335
747 747 When a directory containing a tracked file gets symlinked, as of 5.8
748 748 `hg st` only gives the correct answer about clean (or deleted) files
749 749 if also listing unknowns.
750 750 The tree-based dirstate and status algorithm fix this:
751 751
752 752 #if symlink no-dirstate-v1
753 753
754 754 $ cd ..
755 755 $ hg init issue6335
756 756 $ cd issue6335
757 757 $ mkdir foo
758 758 $ touch foo/a
759 759 $ hg ci -Ama
760 760 adding foo/a
761 761 $ mv foo bar
762 762 $ ln -s bar foo
763 763 $ hg status
764 764 ! foo/a
765 765 ? bar/a
766 766 ? foo
767 767
768 768 $ hg status -c # incorrect output with `dirstate-v1`
769 769 $ hg status -cu
770 770 ? bar/a
771 771 ? foo
772 772 $ hg status -d # incorrect output with `dirstate-v1`
773 773 ! foo/a
774 774 $ hg status -du
775 775 ! foo/a
776 776 ? bar/a
777 777 ? foo
778 778
779 779 #endif
780 780
781 781
782 782 Create a repo with files in each possible status
783 783
784 784 $ cd ..
785 785 $ hg init repo7
786 786 $ cd repo7
787 787 $ mkdir subdir
788 788 $ touch clean modified deleted removed
789 789 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
790 790 $ echo ignored > .hgignore
791 791 $ hg ci -Aqm '#0'
792 792 $ echo 1 > modified
793 793 $ echo 1 > subdir/modified
794 794 $ rm deleted
795 795 $ rm subdir/deleted
796 796 $ hg rm removed
797 797 $ hg rm subdir/removed
798 798 $ touch unknown ignored
799 799 $ touch subdir/unknown subdir/ignored
800 800
801 801 Check the output
802 802
803 803 $ hg status
804 804 M modified
805 805 M subdir/modified
806 806 R removed
807 807 R subdir/removed
808 808 ! deleted
809 809 ! subdir/deleted
810 810 ? subdir/unknown
811 811 ? unknown
812 812
813 813 $ hg status -mard
814 814 M modified
815 815 M subdir/modified
816 816 R removed
817 817 R subdir/removed
818 818 ! deleted
819 819 ! subdir/deleted
820 820
821 821 $ hg status -A
822 822 M modified
823 823 M subdir/modified
824 824 R removed
825 825 R subdir/removed
826 826 ! deleted
827 827 ! subdir/deleted
828 828 ? subdir/unknown
829 829 ? unknown
830 830 I ignored
831 831 I subdir/ignored
832 832 C .hgignore
833 833 C clean
834 834 C subdir/clean
835 835
836 836 Note: `hg status some-name` creates a patternmatcher which is not supported
837 837 yet by the Rust implementation of status, but includematcher is supported.
838 838 --include is used below for that reason
839 839
840 840 #if unix-permissions
841 841
842 842 Not having permission to read a directory that contains tracked files makes
843 843 status emit a warning then behave as if the directory was empty or removed
844 844 entirely:
845 845
846 846 $ chmod 0 subdir
847 847 $ hg status --include subdir
848 848 subdir: Permission denied
849 849 R subdir/removed
850 850 ! subdir/clean
851 851 ! subdir/deleted
852 852 ! subdir/modified
853 853 $ chmod 755 subdir
854 854
855 855 #endif
856 856
857 857 Remove a directory that contains tracked files
858 858
859 859 $ rm -r subdir
860 860 $ hg status --include subdir
861 861 R subdir/removed
862 862 ! subdir/clean
863 863 ! subdir/deleted
864 864 ! subdir/modified
865 865
866 866 and replace it by a file
867 867
868 868 $ touch subdir
869 869 $ hg status --include subdir
870 870 R subdir/removed
871 871 ! subdir/clean
872 872 ! subdir/deleted
873 873 ! subdir/modified
874 874 ? subdir
875 875
876 876 Replaced a deleted or removed file with a directory
877 877
878 878 $ mkdir deleted removed
879 879 $ touch deleted/1 removed/1
880 880 $ hg status --include deleted --include removed
881 881 R removed
882 882 ! deleted
883 883 ? deleted/1
884 884 ? removed/1
885 885 $ hg add removed/1
886 886 $ hg status --include deleted --include removed
887 887 A removed/1
888 888 R removed
889 889 ! deleted
890 890 ? deleted/1
891 891
892 892 Deeply nested files in an ignored directory are still listed on request
893 893
894 894 $ echo ignored-dir >> .hgignore
895 895 $ mkdir ignored-dir
896 896 $ mkdir ignored-dir/subdir
897 897 $ touch ignored-dir/subdir/1
898 898 $ hg status --ignored
899 899 I ignored
900 900 I ignored-dir/subdir/1
901 901
902 902 Check using include flag while listing ignored composes correctly (issue6514)
903 903
904 904 $ cd ..
905 905 $ hg init issue6514
906 906 $ cd issue6514
907 907 $ mkdir ignored-folder
908 908 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
909 909 $ cat >.hgignore <<EOF
910 910 > A.hs
911 911 > B.hs
912 912 > ignored-folder/
913 913 > EOF
914 914 $ hg st -i -I 're:.*\.hs$'
915 915 I A.hs
916 916 I B.hs
917 917 I ignored-folder/ctest.hs
918 918
919 919 #if dirstate-v2
920 920
921 921 Check read_dir caching
922 922
923 923 $ cd ..
924 924 $ hg init repo8
925 925 $ cd repo8
926 926 $ mkdir subdir
927 927 $ touch subdir/a subdir/b
928 928 $ hg ci -Aqm '#0'
929 929
930 930 The cached mtime is initially unset
931 931
932 932 $ hg debugdirstate --dirs --no-dates | grep '^d'
933 933 d 0 0 unset subdir
934 934
935 935 It is still not set when there are unknown files
936 936
937 937 $ touch subdir/unknown
938 938 $ hg status
939 939 ? subdir/unknown
940 940 $ hg debugdirstate --dirs --no-dates | grep '^d'
941 941 d 0 0 unset subdir
942 942
943 943 Now the directory is eligible for caching, so its mtime is save in the dirstate
944 944
945 945 $ rm subdir/unknown
946 946 $ hg status
947 947 $ hg debugdirstate --dirs --no-dates | grep '^d'
948 948 d 0 0 set subdir
949 949
950 950 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
951 951
952 952 $ hg status
953 953
954 954 Creating a new file changes the directorys mtime, invalidating the cache
955 955
956 956 $ touch subdir/unknown
957 957 $ hg status
958 958 ? subdir/unknown
959 959
960 $ rm subdir/unknown
961 $ hg status
962
963 Removing a node from the dirstate resets the cache for its parent directory
964
965 $ hg forget subdir/a
966 $ hg debugdirstate --dirs --no-dates | grep '^d'
967 d 0 0 set subdir
968 $ hg ci -qm '#1'
969 $ hg debugdirstate --dirs --no-dates | grep '^d'
970 d 0 0 unset subdir
971 $ hg status
972 ? subdir/a
973
960 974 #endif
General Comments 0
You need to be logged in to leave comments. Login now