##// END OF EJS Templates
dirstate-item: use the properties in dirstatemap...
marmoute -
r48331:b8013cb7 default
parent child Browse files
Show More
@@ -1,687 +1,689 b''
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 elif entry[0] == b'n' and entry.from_p2:
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 if entry is not None and entry[0] != b'r' and "_dirs" in self.__dict__:
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 if e is not None and e[0] == b'n' and e[3] == now:
264 self._map[f] = DirstateItem(e[0], e[1], e[2], AMBIGUOUS_TIME)
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 )
265 267 self.nonnormalset.add(f)
266 268
267 269 def nonnormalentries(self):
268 270 '''Compute the nonnormal dirstate entries from the dmap'''
269 271 try:
270 272 return parsers.nonnormalotherparententries(self._map)
271 273 except AttributeError:
272 274 nonnorm = set()
273 275 otherparent = set()
274 276 for fname, e in pycompat.iteritems(self._map):
275 if e[0] != b'n' or e[3] == AMBIGUOUS_TIME:
277 if e.state != b'n' or e.mtime == AMBIGUOUS_TIME:
276 278 nonnorm.add(fname)
277 if e[0] == b'n' and e[2] == FROM_P2:
279 if e.from_p2:
278 280 otherparent.add(fname)
279 281 return nonnorm, otherparent
280 282
281 283 @propertycache
282 284 def filefoldmap(self):
283 285 """Returns a dictionary mapping normalized case paths to their
284 286 non-normalized versions.
285 287 """
286 288 try:
287 289 makefilefoldmap = parsers.make_file_foldmap
288 290 except AttributeError:
289 291 pass
290 292 else:
291 293 return makefilefoldmap(
292 294 self._map, util.normcasespec, util.normcasefallback
293 295 )
294 296
295 297 f = {}
296 298 normcase = util.normcase
297 299 for name, s in pycompat.iteritems(self._map):
298 if s[0] != b'r':
300 if not s.removed:
299 301 f[normcase(name)] = name
300 302 f[b'.'] = b'.' # prevents useless util.fspath() invocation
301 303 return f
302 304
303 305 def hastrackeddir(self, d):
304 306 """
305 307 Returns True if the dirstate contains a tracked (not removed) file
306 308 in this directory.
307 309 """
308 310 return d in self._dirs
309 311
310 312 def hasdir(self, d):
311 313 """
312 314 Returns True if the dirstate contains a file (tracked or removed)
313 315 in this directory.
314 316 """
315 317 return d in self._alldirs
316 318
317 319 @propertycache
318 320 def _dirs(self):
319 321 return pathutil.dirs(self._map, b'r')
320 322
321 323 @propertycache
322 324 def _alldirs(self):
323 325 return pathutil.dirs(self._map)
324 326
325 327 def _opendirstatefile(self):
326 328 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
327 329 if self._pendingmode is not None and self._pendingmode != mode:
328 330 fp.close()
329 331 raise error.Abort(
330 332 _(b'working directory state may be changed parallelly')
331 333 )
332 334 self._pendingmode = mode
333 335 return fp
334 336
335 337 def parents(self):
336 338 if not self._parents:
337 339 try:
338 340 fp = self._opendirstatefile()
339 341 st = fp.read(2 * self._nodelen)
340 342 fp.close()
341 343 except IOError as err:
342 344 if err.errno != errno.ENOENT:
343 345 raise
344 346 # File doesn't exist, so the current state is empty
345 347 st = b''
346 348
347 349 l = len(st)
348 350 if l == self._nodelen * 2:
349 351 self._parents = (
350 352 st[: self._nodelen],
351 353 st[self._nodelen : 2 * self._nodelen],
352 354 )
353 355 elif l == 0:
354 356 self._parents = (
355 357 self._nodeconstants.nullid,
356 358 self._nodeconstants.nullid,
357 359 )
358 360 else:
359 361 raise error.Abort(
360 362 _(b'working directory state appears damaged!')
361 363 )
362 364
363 365 return self._parents
364 366
365 367 def setparents(self, p1, p2):
366 368 self._parents = (p1, p2)
367 369 self._dirtyparents = True
368 370
369 371 def read(self):
370 372 # ignore HG_PENDING because identity is used only for writing
371 373 self.identity = util.filestat.frompath(
372 374 self._opener.join(self._filename)
373 375 )
374 376
375 377 try:
376 378 fp = self._opendirstatefile()
377 379 try:
378 380 st = fp.read()
379 381 finally:
380 382 fp.close()
381 383 except IOError as err:
382 384 if err.errno != errno.ENOENT:
383 385 raise
384 386 return
385 387 if not st:
386 388 return
387 389
388 390 if util.safehasattr(parsers, b'dict_new_presized'):
389 391 # Make an estimate of the number of files in the dirstate based on
390 392 # its size. This trades wasting some memory for avoiding costly
391 393 # resizes. Each entry have a prefix of 17 bytes followed by one or
392 394 # two path names. Studies on various large-scale real-world repositories
393 395 # found 54 bytes a reasonable upper limit for the average path names.
394 396 # Copy entries are ignored for the sake of this estimate.
395 397 self._map = parsers.dict_new_presized(len(st) // 71)
396 398
397 399 # Python's garbage collector triggers a GC each time a certain number
398 400 # of container objects (the number being defined by
399 401 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
400 402 # for each file in the dirstate. The C version then immediately marks
401 403 # them as not to be tracked by the collector. However, this has no
402 404 # effect on when GCs are triggered, only on what objects the GC looks
403 405 # into. This means that O(number of files) GCs are unavoidable.
404 406 # Depending on when in the process's lifetime the dirstate is parsed,
405 407 # this can get very expensive. As a workaround, disable GC while
406 408 # parsing the dirstate.
407 409 #
408 410 # (we cannot decorate the function directly since it is in a C module)
409 411 parse_dirstate = util.nogc(parsers.parse_dirstate)
410 412 p = parse_dirstate(self._map, self.copymap, st)
411 413 if not self._dirtyparents:
412 414 self.setparents(*p)
413 415
414 416 # Avoid excess attribute lookups by fast pathing certain checks
415 417 self.__contains__ = self._map.__contains__
416 418 self.__getitem__ = self._map.__getitem__
417 419 self.get = self._map.get
418 420
419 421 def write(self, st, now):
420 422 st.write(
421 423 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
422 424 )
423 425 st.close()
424 426 self._dirtyparents = False
425 427 self.nonnormalset, self.otherparentset = self.nonnormalentries()
426 428
427 429 @propertycache
428 430 def nonnormalset(self):
429 431 nonnorm, otherparents = self.nonnormalentries()
430 432 self.otherparentset = otherparents
431 433 return nonnorm
432 434
433 435 @propertycache
434 436 def otherparentset(self):
435 437 nonnorm, otherparents = self.nonnormalentries()
436 438 self.nonnormalset = nonnorm
437 439 return otherparents
438 440
439 441 def non_normal_or_other_parent_paths(self):
440 442 return self.nonnormalset.union(self.otherparentset)
441 443
442 444 @propertycache
443 445 def identity(self):
444 446 self._map
445 447 return self.identity
446 448
447 449 @propertycache
448 450 def dirfoldmap(self):
449 451 f = {}
450 452 normcase = util.normcase
451 453 for name in self._dirs:
452 454 f[normcase(name)] = name
453 455 return f
454 456
455 457
456 458 if rustmod is not None:
457 459
458 460 class dirstatemap(object):
459 461 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
460 462 self._use_dirstate_v2 = use_dirstate_v2
461 463 self._nodeconstants = nodeconstants
462 464 self._ui = ui
463 465 self._opener = opener
464 466 self._root = root
465 467 self._filename = b'dirstate'
466 468 self._nodelen = 20 # Also update Rust code when changing this!
467 469 self._parents = None
468 470 self._dirtyparents = False
469 471
470 472 # for consistent view between _pl() and _read() invocations
471 473 self._pendingmode = None
472 474
473 475 self._use_dirstate_tree = self._ui.configbool(
474 476 b"experimental",
475 477 b"dirstate-tree.in-memory",
476 478 False,
477 479 )
478 480
479 481 def addfile(
480 482 self,
481 483 f,
482 484 mode=0,
483 485 size=None,
484 486 mtime=None,
485 487 added=False,
486 488 merged=False,
487 489 from_p2=False,
488 490 possibly_dirty=False,
489 491 ):
490 492 return self._rustmap.addfile(
491 493 f,
492 494 mode,
493 495 size,
494 496 mtime,
495 497 added,
496 498 merged,
497 499 from_p2,
498 500 possibly_dirty,
499 501 )
500 502
501 503 def removefile(self, *args, **kwargs):
502 504 return self._rustmap.removefile(*args, **kwargs)
503 505
504 506 def dropfile(self, *args, **kwargs):
505 507 return self._rustmap.dropfile(*args, **kwargs)
506 508
507 509 def clearambiguoustimes(self, *args, **kwargs):
508 510 return self._rustmap.clearambiguoustimes(*args, **kwargs)
509 511
510 512 def nonnormalentries(self):
511 513 return self._rustmap.nonnormalentries()
512 514
513 515 def get(self, *args, **kwargs):
514 516 return self._rustmap.get(*args, **kwargs)
515 517
516 518 @property
517 519 def copymap(self):
518 520 return self._rustmap.copymap()
519 521
520 522 def directories(self):
521 523 return self._rustmap.directories()
522 524
523 525 def preload(self):
524 526 self._rustmap
525 527
526 528 def clear(self):
527 529 self._rustmap.clear()
528 530 self.setparents(
529 531 self._nodeconstants.nullid, self._nodeconstants.nullid
530 532 )
531 533 util.clearcachedproperty(self, b"_dirs")
532 534 util.clearcachedproperty(self, b"_alldirs")
533 535 util.clearcachedproperty(self, b"dirfoldmap")
534 536
535 537 def items(self):
536 538 return self._rustmap.items()
537 539
538 540 def keys(self):
539 541 return iter(self._rustmap)
540 542
541 543 def __contains__(self, key):
542 544 return key in self._rustmap
543 545
544 546 def __getitem__(self, item):
545 547 return self._rustmap[item]
546 548
547 549 def __len__(self):
548 550 return len(self._rustmap)
549 551
550 552 def __iter__(self):
551 553 return iter(self._rustmap)
552 554
553 555 # forward for python2,3 compat
554 556 iteritems = items
555 557
556 558 def _opendirstatefile(self):
557 559 fp, mode = txnutil.trypending(
558 560 self._root, self._opener, self._filename
559 561 )
560 562 if self._pendingmode is not None and self._pendingmode != mode:
561 563 fp.close()
562 564 raise error.Abort(
563 565 _(b'working directory state may be changed parallelly')
564 566 )
565 567 self._pendingmode = mode
566 568 return fp
567 569
568 570 def setparents(self, p1, p2):
569 571 self._parents = (p1, p2)
570 572 self._dirtyparents = True
571 573
572 574 def parents(self):
573 575 if not self._parents:
574 576 if self._use_dirstate_v2:
575 577 offset = len(rustmod.V2_FORMAT_MARKER)
576 578 else:
577 579 offset = 0
578 580 read_len = offset + self._nodelen * 2
579 581 try:
580 582 fp = self._opendirstatefile()
581 583 st = fp.read(read_len)
582 584 fp.close()
583 585 except IOError as err:
584 586 if err.errno != errno.ENOENT:
585 587 raise
586 588 # File doesn't exist, so the current state is empty
587 589 st = b''
588 590
589 591 l = len(st)
590 592 if l == read_len:
591 593 st = st[offset:]
592 594 self._parents = (
593 595 st[: self._nodelen],
594 596 st[self._nodelen : 2 * self._nodelen],
595 597 )
596 598 elif l == 0:
597 599 self._parents = (
598 600 self._nodeconstants.nullid,
599 601 self._nodeconstants.nullid,
600 602 )
601 603 else:
602 604 raise error.Abort(
603 605 _(b'working directory state appears damaged!')
604 606 )
605 607
606 608 return self._parents
607 609
608 610 @propertycache
609 611 def _rustmap(self):
610 612 """
611 613 Fills the Dirstatemap when called.
612 614 """
613 615 # ignore HG_PENDING because identity is used only for writing
614 616 self.identity = util.filestat.frompath(
615 617 self._opener.join(self._filename)
616 618 )
617 619
618 620 try:
619 621 fp = self._opendirstatefile()
620 622 try:
621 623 st = fp.read()
622 624 finally:
623 625 fp.close()
624 626 except IOError as err:
625 627 if err.errno != errno.ENOENT:
626 628 raise
627 629 st = b''
628 630
629 631 self._rustmap, parents = rustmod.DirstateMap.new(
630 632 self._use_dirstate_tree, self._use_dirstate_v2, st
631 633 )
632 634
633 635 if parents and not self._dirtyparents:
634 636 self.setparents(*parents)
635 637
636 638 self.__contains__ = self._rustmap.__contains__
637 639 self.__getitem__ = self._rustmap.__getitem__
638 640 self.get = self._rustmap.get
639 641 return self._rustmap
640 642
641 643 def write(self, st, now):
642 644 parents = self.parents()
643 645 packed = self._rustmap.write(
644 646 self._use_dirstate_v2, parents[0], parents[1], now
645 647 )
646 648 st.write(packed)
647 649 st.close()
648 650 self._dirtyparents = False
649 651
650 652 @propertycache
651 653 def filefoldmap(self):
652 654 """Returns a dictionary mapping normalized case paths to their
653 655 non-normalized versions.
654 656 """
655 657 return self._rustmap.filefoldmapasdict()
656 658
657 659 def hastrackeddir(self, d):
658 660 return self._rustmap.hastrackeddir(d)
659 661
660 662 def hasdir(self, d):
661 663 return self._rustmap.hasdir(d)
662 664
663 665 @propertycache
664 666 def identity(self):
665 667 self._rustmap
666 668 return self.identity
667 669
668 670 @property
669 671 def nonnormalset(self):
670 672 nonnorm = self._rustmap.non_normal_entries()
671 673 return nonnorm
672 674
673 675 @propertycache
674 676 def otherparentset(self):
675 677 otherparents = self._rustmap.other_parent_entries()
676 678 return otherparents
677 679
678 680 def non_normal_or_other_parent_paths(self):
679 681 return self._rustmap.non_normal_or_other_parent_paths()
680 682
681 683 @propertycache
682 684 def dirfoldmap(self):
683 685 f = {}
684 686 normcase = util.normcase
685 687 for name, _pseudo_entry in self.directories():
686 688 f[normcase(name)] = name
687 689 return f
General Comments 0
You need to be logged in to leave comments. Login now