##// END OF EJS Templates
dirstatemap: use `set_possibly_dirty` in `clearambiguoustimes`...
marmoute -
r48468:724a7797 default
parent child Browse files
Show More
@@ -1,689 +1,687
1 1 # dirstatemap.py
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 import errno
9 9
10 10 from .i18n import _
11 11
12 12 from . import (
13 13 error,
14 14 pathutil,
15 15 policy,
16 16 pycompat,
17 17 txnutil,
18 18 util,
19 19 )
20 20
21 21 parsers = policy.importmod('parsers')
22 22 rustmod = policy.importrust('dirstate')
23 23
24 24 propertycache = util.propertycache
25 25
26 26 DirstateItem = parsers.DirstateItem
27 27
28 28
29 29 # a special value used internally for `size` if the file come from the other parent
30 30 FROM_P2 = -2
31 31
32 32 # a special value used internally for `size` if the file is modified/merged/added
33 33 NONNORMAL = -1
34 34
35 35 # a special value used internally for `time` if the time is ambigeous
36 36 AMBIGUOUS_TIME = -1
37 37
38 38 rangemask = 0x7FFFFFFF
39 39
40 40
41 41 class dirstatemap(object):
42 42 """Map encapsulating the dirstate's contents.
43 43
44 44 The dirstate contains the following state:
45 45
46 46 - `identity` is the identity of the dirstate file, which can be used to
47 47 detect when changes have occurred to the dirstate file.
48 48
49 49 - `parents` is a pair containing the parents of the working copy. The
50 50 parents are updated by calling `setparents`.
51 51
52 52 - the state map maps filenames to tuples of (state, mode, size, mtime),
53 53 where state is a single character representing 'normal', 'added',
54 54 'removed', or 'merged'. It is read by treating the dirstate as a
55 55 dict. File state is updated by calling the `addfile`, `removefile` and
56 56 `dropfile` methods.
57 57
58 58 - `copymap` maps destination filenames to their source filename.
59 59
60 60 The dirstate also provides the following views onto the state:
61 61
62 62 - `nonnormalset` is a set of the filenames that have state other
63 63 than 'normal', or are normal but have an mtime of -1 ('normallookup').
64 64
65 65 - `otherparentset` is a set of the filenames that are marked as coming
66 66 from the second parent when the dirstate is currently being merged.
67 67
68 68 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
69 69 form that they appear as in the dirstate.
70 70
71 71 - `dirfoldmap` is a dict mapping normalized directory names to the
72 72 denormalized form that they appear as in the dirstate.
73 73 """
74 74
75 75 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
76 76 self._ui = ui
77 77 self._opener = opener
78 78 self._root = root
79 79 self._filename = b'dirstate'
80 80 self._nodelen = 20
81 81 self._nodeconstants = nodeconstants
82 82 assert (
83 83 not use_dirstate_v2
84 84 ), "should have detected unsupported requirement"
85 85
86 86 self._parents = None
87 87 self._dirtyparents = False
88 88
89 89 # for consistent view between _pl() and _read() invocations
90 90 self._pendingmode = None
91 91
92 92 @propertycache
93 93 def _map(self):
94 94 self._map = {}
95 95 self.read()
96 96 return self._map
97 97
98 98 @propertycache
99 99 def copymap(self):
100 100 self.copymap = {}
101 101 self._map
102 102 return self.copymap
103 103
104 104 def directories(self):
105 105 # Rust / dirstate-v2 only
106 106 return []
107 107
108 108 def clear(self):
109 109 self._map.clear()
110 110 self.copymap.clear()
111 111 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
112 112 util.clearcachedproperty(self, b"_dirs")
113 113 util.clearcachedproperty(self, b"_alldirs")
114 114 util.clearcachedproperty(self, b"filefoldmap")
115 115 util.clearcachedproperty(self, b"dirfoldmap")
116 116 util.clearcachedproperty(self, b"nonnormalset")
117 117 util.clearcachedproperty(self, b"otherparentset")
118 118
119 119 def items(self):
120 120 return pycompat.iteritems(self._map)
121 121
122 122 # forward for python2,3 compat
123 123 iteritems = items
124 124
125 125 def __len__(self):
126 126 return len(self._map)
127 127
128 128 def __iter__(self):
129 129 return iter(self._map)
130 130
131 131 def get(self, key, default=None):
132 132 return self._map.get(key, default)
133 133
134 134 def __contains__(self, key):
135 135 return key in self._map
136 136
137 137 def __getitem__(self, key):
138 138 return self._map[key]
139 139
140 140 def keys(self):
141 141 return self._map.keys()
142 142
143 143 def preload(self):
144 144 """Loads the underlying data, if it's not already loaded"""
145 145 self._map
146 146
147 147 def addfile(
148 148 self,
149 149 f,
150 150 mode=0,
151 151 size=None,
152 152 mtime=None,
153 153 added=False,
154 154 merged=False,
155 155 from_p2=False,
156 156 possibly_dirty=False,
157 157 ):
158 158 """Add a tracked file to the dirstate."""
159 159 if added:
160 160 assert not merged
161 161 assert not possibly_dirty
162 162 assert not from_p2
163 163 state = b'a'
164 164 size = NONNORMAL
165 165 mtime = AMBIGUOUS_TIME
166 166 elif merged:
167 167 assert not possibly_dirty
168 168 assert not from_p2
169 169 state = b'm'
170 170 size = FROM_P2
171 171 mtime = AMBIGUOUS_TIME
172 172 elif from_p2:
173 173 assert not possibly_dirty
174 174 state = b'n'
175 175 size = FROM_P2
176 176 mtime = AMBIGUOUS_TIME
177 177 elif possibly_dirty:
178 178 state = b'n'
179 179 size = NONNORMAL
180 180 mtime = AMBIGUOUS_TIME
181 181 else:
182 182 assert size != FROM_P2
183 183 assert size != NONNORMAL
184 184 state = b'n'
185 185 size = size & rangemask
186 186 mtime = mtime & rangemask
187 187 assert state is not None
188 188 assert size is not None
189 189 assert mtime is not None
190 190 old_entry = self.get(f)
191 191 if (
192 192 old_entry is None or old_entry.removed
193 193 ) and "_dirs" in self.__dict__:
194 194 self._dirs.addpath(f)
195 195 if old_entry is None and "_alldirs" in self.__dict__:
196 196 self._alldirs.addpath(f)
197 197 self._map[f] = DirstateItem(state, mode, size, mtime)
198 198 if state != b'n' or mtime == AMBIGUOUS_TIME:
199 199 self.nonnormalset.add(f)
200 200 if size == FROM_P2:
201 201 self.otherparentset.add(f)
202 202
203 203 def removefile(self, f, in_merge=False):
204 204 """
205 205 Mark a file as removed in the dirstate.
206 206
207 207 The `size` parameter is used to store sentinel values that indicate
208 208 the file's previous state. In the future, we should refactor this
209 209 to be more explicit about what that state is.
210 210 """
211 211 entry = self.get(f)
212 212 size = 0
213 213 if in_merge:
214 214 # XXX we should not be able to have 'm' state and 'FROM_P2' if not
215 215 # during a merge. So I (marmoute) am not sure we need the
216 216 # conditionnal at all. Adding double checking this with assert
217 217 # would be nice.
218 218 if entry is not None:
219 219 # backup the previous state
220 220 if entry.merged: # merge
221 221 size = NONNORMAL
222 222 elif entry.from_p2:
223 223 size = FROM_P2
224 224 self.otherparentset.add(f)
225 225 if size == 0:
226 226 self.copymap.pop(f, None)
227 227
228 228 if entry is not None and not entry.removed and "_dirs" in self.__dict__:
229 229 self._dirs.delpath(f)
230 230 if entry is None and "_alldirs" in self.__dict__:
231 231 self._alldirs.addpath(f)
232 232 if "filefoldmap" in self.__dict__:
233 233 normed = util.normcase(f)
234 234 self.filefoldmap.pop(normed, None)
235 235 self._map[f] = DirstateItem(b'r', 0, size, 0)
236 236 self.nonnormalset.add(f)
237 237
238 238 def dropfile(self, f):
239 239 """
240 240 Remove a file from the dirstate. Returns True if the file was
241 241 previously recorded.
242 242 """
243 243 old_entry = self._map.pop(f, None)
244 244 exists = False
245 245 oldstate = b'?'
246 246 if old_entry is not None:
247 247 exists = True
248 248 oldstate = old_entry.state
249 249 if exists:
250 250 if oldstate != b"r" and "_dirs" in self.__dict__:
251 251 self._dirs.delpath(f)
252 252 if "_alldirs" in self.__dict__:
253 253 self._alldirs.delpath(f)
254 254 if "filefoldmap" in self.__dict__:
255 255 normed = util.normcase(f)
256 256 self.filefoldmap.pop(normed, None)
257 257 self.nonnormalset.discard(f)
258 258 return exists
259 259
260 260 def clearambiguoustimes(self, files, now):
261 261 for f in files:
262 262 e = self.get(f)
263 263 if e is not None and e.need_delay(now):
264 self._map[f] = DirstateItem(
265 e.state, e.mode, e.size, AMBIGUOUS_TIME
266 )
264 e.set_possibly_dirty()
267 265 self.nonnormalset.add(f)
268 266
269 267 def nonnormalentries(self):
270 268 '''Compute the nonnormal dirstate entries from the dmap'''
271 269 try:
272 270 return parsers.nonnormalotherparententries(self._map)
273 271 except AttributeError:
274 272 nonnorm = set()
275 273 otherparent = set()
276 274 for fname, e in pycompat.iteritems(self._map):
277 275 if e.state != b'n' or e.mtime == AMBIGUOUS_TIME:
278 276 nonnorm.add(fname)
279 277 if e.from_p2:
280 278 otherparent.add(fname)
281 279 return nonnorm, otherparent
282 280
283 281 @propertycache
284 282 def filefoldmap(self):
285 283 """Returns a dictionary mapping normalized case paths to their
286 284 non-normalized versions.
287 285 """
288 286 try:
289 287 makefilefoldmap = parsers.make_file_foldmap
290 288 except AttributeError:
291 289 pass
292 290 else:
293 291 return makefilefoldmap(
294 292 self._map, util.normcasespec, util.normcasefallback
295 293 )
296 294
297 295 f = {}
298 296 normcase = util.normcase
299 297 for name, s in pycompat.iteritems(self._map):
300 298 if not s.removed:
301 299 f[normcase(name)] = name
302 300 f[b'.'] = b'.' # prevents useless util.fspath() invocation
303 301 return f
304 302
305 303 def hastrackeddir(self, d):
306 304 """
307 305 Returns True if the dirstate contains a tracked (not removed) file
308 306 in this directory.
309 307 """
310 308 return d in self._dirs
311 309
312 310 def hasdir(self, d):
313 311 """
314 312 Returns True if the dirstate contains a file (tracked or removed)
315 313 in this directory.
316 314 """
317 315 return d in self._alldirs
318 316
319 317 @propertycache
320 318 def _dirs(self):
321 319 return pathutil.dirs(self._map, b'r')
322 320
323 321 @propertycache
324 322 def _alldirs(self):
325 323 return pathutil.dirs(self._map)
326 324
327 325 def _opendirstatefile(self):
328 326 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
329 327 if self._pendingmode is not None and self._pendingmode != mode:
330 328 fp.close()
331 329 raise error.Abort(
332 330 _(b'working directory state may be changed parallelly')
333 331 )
334 332 self._pendingmode = mode
335 333 return fp
336 334
337 335 def parents(self):
338 336 if not self._parents:
339 337 try:
340 338 fp = self._opendirstatefile()
341 339 st = fp.read(2 * self._nodelen)
342 340 fp.close()
343 341 except IOError as err:
344 342 if err.errno != errno.ENOENT:
345 343 raise
346 344 # File doesn't exist, so the current state is empty
347 345 st = b''
348 346
349 347 l = len(st)
350 348 if l == self._nodelen * 2:
351 349 self._parents = (
352 350 st[: self._nodelen],
353 351 st[self._nodelen : 2 * self._nodelen],
354 352 )
355 353 elif l == 0:
356 354 self._parents = (
357 355 self._nodeconstants.nullid,
358 356 self._nodeconstants.nullid,
359 357 )
360 358 else:
361 359 raise error.Abort(
362 360 _(b'working directory state appears damaged!')
363 361 )
364 362
365 363 return self._parents
366 364
367 365 def setparents(self, p1, p2):
368 366 self._parents = (p1, p2)
369 367 self._dirtyparents = True
370 368
371 369 def read(self):
372 370 # ignore HG_PENDING because identity is used only for writing
373 371 self.identity = util.filestat.frompath(
374 372 self._opener.join(self._filename)
375 373 )
376 374
377 375 try:
378 376 fp = self._opendirstatefile()
379 377 try:
380 378 st = fp.read()
381 379 finally:
382 380 fp.close()
383 381 except IOError as err:
384 382 if err.errno != errno.ENOENT:
385 383 raise
386 384 return
387 385 if not st:
388 386 return
389 387
390 388 if util.safehasattr(parsers, b'dict_new_presized'):
391 389 # Make an estimate of the number of files in the dirstate based on
392 390 # its size. This trades wasting some memory for avoiding costly
393 391 # resizes. Each entry have a prefix of 17 bytes followed by one or
394 392 # two path names. Studies on various large-scale real-world repositories
395 393 # found 54 bytes a reasonable upper limit for the average path names.
396 394 # Copy entries are ignored for the sake of this estimate.
397 395 self._map = parsers.dict_new_presized(len(st) // 71)
398 396
399 397 # Python's garbage collector triggers a GC each time a certain number
400 398 # of container objects (the number being defined by
401 399 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
402 400 # for each file in the dirstate. The C version then immediately marks
403 401 # them as not to be tracked by the collector. However, this has no
404 402 # effect on when GCs are triggered, only on what objects the GC looks
405 403 # into. This means that O(number of files) GCs are unavoidable.
406 404 # Depending on when in the process's lifetime the dirstate is parsed,
407 405 # this can get very expensive. As a workaround, disable GC while
408 406 # parsing the dirstate.
409 407 #
410 408 # (we cannot decorate the function directly since it is in a C module)
411 409 parse_dirstate = util.nogc(parsers.parse_dirstate)
412 410 p = parse_dirstate(self._map, self.copymap, st)
413 411 if not self._dirtyparents:
414 412 self.setparents(*p)
415 413
416 414 # Avoid excess attribute lookups by fast pathing certain checks
417 415 self.__contains__ = self._map.__contains__
418 416 self.__getitem__ = self._map.__getitem__
419 417 self.get = self._map.get
420 418
421 419 def write(self, st, now):
422 420 st.write(
423 421 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
424 422 )
425 423 st.close()
426 424 self._dirtyparents = False
427 425 self.nonnormalset, self.otherparentset = self.nonnormalentries()
428 426
429 427 @propertycache
430 428 def nonnormalset(self):
431 429 nonnorm, otherparents = self.nonnormalentries()
432 430 self.otherparentset = otherparents
433 431 return nonnorm
434 432
435 433 @propertycache
436 434 def otherparentset(self):
437 435 nonnorm, otherparents = self.nonnormalentries()
438 436 self.nonnormalset = nonnorm
439 437 return otherparents
440 438
441 439 def non_normal_or_other_parent_paths(self):
442 440 return self.nonnormalset.union(self.otherparentset)
443 441
444 442 @propertycache
445 443 def identity(self):
446 444 self._map
447 445 return self.identity
448 446
449 447 @propertycache
450 448 def dirfoldmap(self):
451 449 f = {}
452 450 normcase = util.normcase
453 451 for name in self._dirs:
454 452 f[normcase(name)] = name
455 453 return f
456 454
457 455
458 456 if rustmod is not None:
459 457
460 458 class dirstatemap(object):
461 459 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
462 460 self._use_dirstate_v2 = use_dirstate_v2
463 461 self._nodeconstants = nodeconstants
464 462 self._ui = ui
465 463 self._opener = opener
466 464 self._root = root
467 465 self._filename = b'dirstate'
468 466 self._nodelen = 20 # Also update Rust code when changing this!
469 467 self._parents = None
470 468 self._dirtyparents = False
471 469
472 470 # for consistent view between _pl() and _read() invocations
473 471 self._pendingmode = None
474 472
475 473 self._use_dirstate_tree = self._ui.configbool(
476 474 b"experimental",
477 475 b"dirstate-tree.in-memory",
478 476 False,
479 477 )
480 478
481 479 def addfile(
482 480 self,
483 481 f,
484 482 mode=0,
485 483 size=None,
486 484 mtime=None,
487 485 added=False,
488 486 merged=False,
489 487 from_p2=False,
490 488 possibly_dirty=False,
491 489 ):
492 490 return self._rustmap.addfile(
493 491 f,
494 492 mode,
495 493 size,
496 494 mtime,
497 495 added,
498 496 merged,
499 497 from_p2,
500 498 possibly_dirty,
501 499 )
502 500
503 501 def removefile(self, *args, **kwargs):
504 502 return self._rustmap.removefile(*args, **kwargs)
505 503
506 504 def dropfile(self, *args, **kwargs):
507 505 return self._rustmap.dropfile(*args, **kwargs)
508 506
509 507 def clearambiguoustimes(self, *args, **kwargs):
510 508 return self._rustmap.clearambiguoustimes(*args, **kwargs)
511 509
512 510 def nonnormalentries(self):
513 511 return self._rustmap.nonnormalentries()
514 512
515 513 def get(self, *args, **kwargs):
516 514 return self._rustmap.get(*args, **kwargs)
517 515
518 516 @property
519 517 def copymap(self):
520 518 return self._rustmap.copymap()
521 519
522 520 def directories(self):
523 521 return self._rustmap.directories()
524 522
525 523 def preload(self):
526 524 self._rustmap
527 525
528 526 def clear(self):
529 527 self._rustmap.clear()
530 528 self.setparents(
531 529 self._nodeconstants.nullid, self._nodeconstants.nullid
532 530 )
533 531 util.clearcachedproperty(self, b"_dirs")
534 532 util.clearcachedproperty(self, b"_alldirs")
535 533 util.clearcachedproperty(self, b"dirfoldmap")
536 534
537 535 def items(self):
538 536 return self._rustmap.items()
539 537
540 538 def keys(self):
541 539 return iter(self._rustmap)
542 540
543 541 def __contains__(self, key):
544 542 return key in self._rustmap
545 543
546 544 def __getitem__(self, item):
547 545 return self._rustmap[item]
548 546
549 547 def __len__(self):
550 548 return len(self._rustmap)
551 549
552 550 def __iter__(self):
553 551 return iter(self._rustmap)
554 552
555 553 # forward for python2,3 compat
556 554 iteritems = items
557 555
558 556 def _opendirstatefile(self):
559 557 fp, mode = txnutil.trypending(
560 558 self._root, self._opener, self._filename
561 559 )
562 560 if self._pendingmode is not None and self._pendingmode != mode:
563 561 fp.close()
564 562 raise error.Abort(
565 563 _(b'working directory state may be changed parallelly')
566 564 )
567 565 self._pendingmode = mode
568 566 return fp
569 567
570 568 def setparents(self, p1, p2):
571 569 self._parents = (p1, p2)
572 570 self._dirtyparents = True
573 571
574 572 def parents(self):
575 573 if not self._parents:
576 574 if self._use_dirstate_v2:
577 575 offset = len(rustmod.V2_FORMAT_MARKER)
578 576 else:
579 577 offset = 0
580 578 read_len = offset + self._nodelen * 2
581 579 try:
582 580 fp = self._opendirstatefile()
583 581 st = fp.read(read_len)
584 582 fp.close()
585 583 except IOError as err:
586 584 if err.errno != errno.ENOENT:
587 585 raise
588 586 # File doesn't exist, so the current state is empty
589 587 st = b''
590 588
591 589 l = len(st)
592 590 if l == read_len:
593 591 st = st[offset:]
594 592 self._parents = (
595 593 st[: self._nodelen],
596 594 st[self._nodelen : 2 * self._nodelen],
597 595 )
598 596 elif l == 0:
599 597 self._parents = (
600 598 self._nodeconstants.nullid,
601 599 self._nodeconstants.nullid,
602 600 )
603 601 else:
604 602 raise error.Abort(
605 603 _(b'working directory state appears damaged!')
606 604 )
607 605
608 606 return self._parents
609 607
610 608 @propertycache
611 609 def _rustmap(self):
612 610 """
613 611 Fills the Dirstatemap when called.
614 612 """
615 613 # ignore HG_PENDING because identity is used only for writing
616 614 self.identity = util.filestat.frompath(
617 615 self._opener.join(self._filename)
618 616 )
619 617
620 618 try:
621 619 fp = self._opendirstatefile()
622 620 try:
623 621 st = fp.read()
624 622 finally:
625 623 fp.close()
626 624 except IOError as err:
627 625 if err.errno != errno.ENOENT:
628 626 raise
629 627 st = b''
630 628
631 629 self._rustmap, parents = rustmod.DirstateMap.new(
632 630 self._use_dirstate_tree, self._use_dirstate_v2, st
633 631 )
634 632
635 633 if parents and not self._dirtyparents:
636 634 self.setparents(*parents)
637 635
638 636 self.__contains__ = self._rustmap.__contains__
639 637 self.__getitem__ = self._rustmap.__getitem__
640 638 self.get = self._rustmap.get
641 639 return self._rustmap
642 640
643 641 def write(self, st, now):
644 642 parents = self.parents()
645 643 packed = self._rustmap.write(
646 644 self._use_dirstate_v2, parents[0], parents[1], now
647 645 )
648 646 st.write(packed)
649 647 st.close()
650 648 self._dirtyparents = False
651 649
652 650 @propertycache
653 651 def filefoldmap(self):
654 652 """Returns a dictionary mapping normalized case paths to their
655 653 non-normalized versions.
656 654 """
657 655 return self._rustmap.filefoldmapasdict()
658 656
659 657 def hastrackeddir(self, d):
660 658 return self._rustmap.hastrackeddir(d)
661 659
662 660 def hasdir(self, d):
663 661 return self._rustmap.hasdir(d)
664 662
665 663 @propertycache
666 664 def identity(self):
667 665 self._rustmap
668 666 return self.identity
669 667
670 668 @property
671 669 def nonnormalset(self):
672 670 nonnorm = self._rustmap.non_normal_entries()
673 671 return nonnorm
674 672
675 673 @propertycache
676 674 def otherparentset(self):
677 675 otherparents = self._rustmap.other_parent_entries()
678 676 return otherparents
679 677
680 678 def non_normal_or_other_parent_paths(self):
681 679 return self._rustmap.non_normal_or_other_parent_paths()
682 680
683 681 @propertycache
684 682 def dirfoldmap(self):
685 683 f = {}
686 684 normcase = util.normcase
687 685 for name, _pseudo_entry in self.directories():
688 686 f[normcase(name)] = name
689 687 return f
General Comments 0
You need to be logged in to leave comments. Login now