##// END OF EJS Templates
dirstatemap: use a common __init__ for dirstatemap...
marmoute -
r48932:fe661771 default
parent child Browse files
Show More
@@ -1,916 +1,913 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 from .dirstateutils import (
22 22 docket as docketmod,
23 23 )
24 24
25 25 parsers = policy.importmod('parsers')
26 26 rustmod = policy.importrust('dirstate')
27 27
28 28 propertycache = util.propertycache
29 29
30 30 if rustmod is None:
31 31 DirstateItem = parsers.DirstateItem
32 32 else:
33 33 DirstateItem = rustmod.DirstateItem
34 34
35 35 rangemask = 0x7FFFFFFF
36 36
37 37
38 38 class _dirstatemapcommon(object):
39 39 """
40 40 Methods that are identical for both implementations of the dirstatemap
41 41 class, with and without Rust extensions enabled.
42 42 """
43 43
44 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
45 self._use_dirstate_v2 = use_dirstate_v2
46 self._nodeconstants = nodeconstants
47 self._ui = ui
48 self._opener = opener
49 self._root = root
50 self._filename = b'dirstate'
51 self._nodelen = 20 # Also update Rust code when changing this!
52 self._parents = None
53 self._dirtyparents = False
54
55 # for consistent view between _pl() and _read() invocations
56 self._pendingmode = None
57
44 58
45 59 class dirstatemap(_dirstatemapcommon):
46 60 """Map encapsulating the dirstate's contents.
47 61
48 62 The dirstate contains the following state:
49 63
50 64 - `identity` is the identity of the dirstate file, which can be used to
51 65 detect when changes have occurred to the dirstate file.
52 66
53 67 - `parents` is a pair containing the parents of the working copy. The
54 68 parents are updated by calling `setparents`.
55 69
56 70 - the state map maps filenames to tuples of (state, mode, size, mtime),
57 71 where state is a single character representing 'normal', 'added',
58 72 'removed', or 'merged'. It is read by treating the dirstate as a
59 73 dict. File state is updated by calling various methods (see each
60 74 documentation for details):
61 75
62 76 - `reset_state`,
63 77 - `set_tracked`
64 78 - `set_untracked`
65 79 - `set_clean`
66 80 - `set_possibly_dirty`
67 81
68 82 - `copymap` maps destination filenames to their source filename.
69 83
70 84 The dirstate also provides the following views onto the state:
71 85
72 86 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
73 87 form that they appear as in the dirstate.
74 88
75 89 - `dirfoldmap` is a dict mapping normalized directory names to the
76 90 denormalized form that they appear as in the dirstate.
77 91 """
78 92
79 93 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
80 self._ui = ui
81 self._opener = opener
82 self._root = root
83 self._filename = b'dirstate'
84 self._nodelen = 20
85 self._nodeconstants = nodeconstants
86 assert (
87 not use_dirstate_v2
88 ), "should have detected unsupported requirement"
89
90 self._parents = None
91 self._dirtyparents = False
92
93 # for consistent view between _pl() and _read() invocations
94 self._pendingmode = None
94 super(dirstatemap, self).__init__(
95 ui, opener, root, nodeconstants, use_dirstate_v2
96 )
97 if self._use_dirstate_v2:
98 msg = "Dirstate V2 not supportedi"
99 msg += "(should have detected unsupported requirement)"
100 raise error.ProgrammingError(msg)
95 101
96 102 @propertycache
97 103 def _map(self):
98 104 self._map = {}
99 105 self.read()
100 106 return self._map
101 107
102 108 @propertycache
103 109 def copymap(self):
104 110 self.copymap = {}
105 111 self._map
106 112 return self.copymap
107 113
108 114 def clear(self):
109 115 self._map.clear()
110 116 self.copymap.clear()
111 117 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
112 118 util.clearcachedproperty(self, b"_dirs")
113 119 util.clearcachedproperty(self, b"_alldirs")
114 120 util.clearcachedproperty(self, b"filefoldmap")
115 121 util.clearcachedproperty(self, b"dirfoldmap")
116 122
117 123 def items(self):
118 124 return pycompat.iteritems(self._map)
119 125
120 126 # forward for python2,3 compat
121 127 iteritems = items
122 128
123 129 def debug_iter(self, all):
124 130 """
125 131 Return an iterator of (filename, state, mode, size, mtime) tuples
126 132
127 133 `all` is unused when Rust is not enabled
128 134 """
129 135 for (filename, item) in self.items():
130 136 yield (filename, item.state, item.mode, item.size, item.mtime)
131 137
132 138 def __len__(self):
133 139 return len(self._map)
134 140
135 141 def __iter__(self):
136 142 return iter(self._map)
137 143
138 144 def get(self, key, default=None):
139 145 return self._map.get(key, default)
140 146
141 147 def __contains__(self, key):
142 148 return key in self._map
143 149
144 150 def __getitem__(self, key):
145 151 return self._map[key]
146 152
147 153 def keys(self):
148 154 return self._map.keys()
149 155
150 156 def preload(self):
151 157 """Loads the underlying data, if it's not already loaded"""
152 158 self._map
153 159
154 160 def _dirs_incr(self, filename, old_entry=None):
155 161 """incremente the dirstate counter if applicable"""
156 162 if (
157 163 old_entry is None or old_entry.removed
158 164 ) and "_dirs" in self.__dict__:
159 165 self._dirs.addpath(filename)
160 166 if old_entry is None and "_alldirs" in self.__dict__:
161 167 self._alldirs.addpath(filename)
162 168
163 169 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
164 170 """decremente the dirstate counter if applicable"""
165 171 if old_entry is not None:
166 172 if "_dirs" in self.__dict__ and not old_entry.removed:
167 173 self._dirs.delpath(filename)
168 174 if "_alldirs" in self.__dict__ and not remove_variant:
169 175 self._alldirs.delpath(filename)
170 176 elif remove_variant and "_alldirs" in self.__dict__:
171 177 self._alldirs.addpath(filename)
172 178 if "filefoldmap" in self.__dict__:
173 179 normed = util.normcase(filename)
174 180 self.filefoldmap.pop(normed, None)
175 181
176 182 def set_possibly_dirty(self, filename):
177 183 """record that the current state of the file on disk is unknown"""
178 184 self[filename].set_possibly_dirty()
179 185
180 186 def set_clean(self, filename, mode, size, mtime):
181 187 """mark a file as back to a clean state"""
182 188 entry = self[filename]
183 189 mtime = mtime & rangemask
184 190 size = size & rangemask
185 191 entry.set_clean(mode, size, mtime)
186 192 self.copymap.pop(filename, None)
187 193
188 194 def reset_state(
189 195 self,
190 196 filename,
191 197 wc_tracked=False,
192 198 p1_tracked=False,
193 199 p2_tracked=False,
194 200 merged=False,
195 201 clean_p1=False,
196 202 clean_p2=False,
197 203 possibly_dirty=False,
198 204 parentfiledata=None,
199 205 ):
200 206 """Set a entry to a given state, diregarding all previous state
201 207
202 208 This is to be used by the part of the dirstate API dedicated to
203 209 adjusting the dirstate after a update/merge.
204 210
205 211 note: calling this might result to no entry existing at all if the
206 212 dirstate map does not see any point at having one for this file
207 213 anymore.
208 214 """
209 215 if merged and (clean_p1 or clean_p2):
210 216 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
211 217 raise error.ProgrammingError(msg)
212 218 # copy information are now outdated
213 219 # (maybe new information should be in directly passed to this function)
214 220 self.copymap.pop(filename, None)
215 221
216 222 if not (p1_tracked or p2_tracked or wc_tracked):
217 223 old_entry = self._map.pop(filename, None)
218 224 self._dirs_decr(filename, old_entry=old_entry)
219 225 self.copymap.pop(filename, None)
220 226 return
221 227 elif merged:
222 228 pass
223 229 elif not (p1_tracked or p2_tracked) and wc_tracked:
224 230 pass # file is added, nothing special to adjust
225 231 elif (p1_tracked or p2_tracked) and not wc_tracked:
226 232 pass
227 233 elif clean_p2 and wc_tracked:
228 234 pass
229 235 elif not p1_tracked and p2_tracked and wc_tracked:
230 236 clean_p2 = True
231 237 elif possibly_dirty:
232 238 pass
233 239 elif wc_tracked:
234 240 # this is a "normal" file
235 241 if parentfiledata is None:
236 242 msg = b'failed to pass parentfiledata for a normal file: %s'
237 243 msg %= filename
238 244 raise error.ProgrammingError(msg)
239 245 else:
240 246 assert False, 'unreachable'
241 247
242 248 old_entry = self._map.get(filename)
243 249 self._dirs_incr(filename, old_entry)
244 250 entry = DirstateItem(
245 251 wc_tracked=wc_tracked,
246 252 p1_tracked=p1_tracked,
247 253 p2_tracked=p2_tracked,
248 254 merged=merged,
249 255 clean_p1=clean_p1,
250 256 clean_p2=clean_p2,
251 257 possibly_dirty=possibly_dirty,
252 258 parentfiledata=parentfiledata,
253 259 )
254 260 self._map[filename] = entry
255 261
256 262 def set_tracked(self, filename):
257 263 new = False
258 264 entry = self.get(filename)
259 265 if entry is None:
260 266 self._dirs_incr(filename)
261 267 entry = DirstateItem(
262 268 p1_tracked=False,
263 269 p2_tracked=False,
264 270 wc_tracked=True,
265 271 merged=False,
266 272 clean_p1=False,
267 273 clean_p2=False,
268 274 possibly_dirty=False,
269 275 parentfiledata=None,
270 276 )
271 277 self._map[filename] = entry
272 278 new = True
273 279 elif not entry.tracked:
274 280 self._dirs_incr(filename, entry)
275 281 entry.set_tracked()
276 282 new = True
277 283 else:
278 284 # XXX This is probably overkill for more case, but we need this to
279 285 # fully replace the `normallookup` call with `set_tracked` one.
280 286 # Consider smoothing this in the future.
281 287 self.set_possibly_dirty(filename)
282 288 return new
283 289
284 290 def set_untracked(self, f):
285 291 """Mark a file as no longer tracked in the dirstate map"""
286 292 entry = self.get(f)
287 293 if entry is None:
288 294 return False
289 295 else:
290 296 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
291 297 if not entry.merged:
292 298 self.copymap.pop(f, None)
293 299 if entry.added:
294 300 self._map.pop(f, None)
295 301 else:
296 302 entry.set_untracked()
297 303 return True
298 304
299 305 @propertycache
300 306 def filefoldmap(self):
301 307 """Returns a dictionary mapping normalized case paths to their
302 308 non-normalized versions.
303 309 """
304 310 try:
305 311 makefilefoldmap = parsers.make_file_foldmap
306 312 except AttributeError:
307 313 pass
308 314 else:
309 315 return makefilefoldmap(
310 316 self._map, util.normcasespec, util.normcasefallback
311 317 )
312 318
313 319 f = {}
314 320 normcase = util.normcase
315 321 for name, s in pycompat.iteritems(self._map):
316 322 if not s.removed:
317 323 f[normcase(name)] = name
318 324 f[b'.'] = b'.' # prevents useless util.fspath() invocation
319 325 return f
320 326
321 327 def hastrackeddir(self, d):
322 328 """
323 329 Returns True if the dirstate contains a tracked (not removed) file
324 330 in this directory.
325 331 """
326 332 return d in self._dirs
327 333
328 334 def hasdir(self, d):
329 335 """
330 336 Returns True if the dirstate contains a file (tracked or removed)
331 337 in this directory.
332 338 """
333 339 return d in self._alldirs
334 340
335 341 @propertycache
336 342 def _dirs(self):
337 343 return pathutil.dirs(self._map, only_tracked=True)
338 344
339 345 @propertycache
340 346 def _alldirs(self):
341 347 return pathutil.dirs(self._map)
342 348
343 349 def _opendirstatefile(self):
344 350 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
345 351 if self._pendingmode is not None and self._pendingmode != mode:
346 352 fp.close()
347 353 raise error.Abort(
348 354 _(b'working directory state may be changed parallelly')
349 355 )
350 356 self._pendingmode = mode
351 357 return fp
352 358
353 359 def parents(self):
354 360 if not self._parents:
355 361 try:
356 362 fp = self._opendirstatefile()
357 363 st = fp.read(2 * self._nodelen)
358 364 fp.close()
359 365 except IOError as err:
360 366 if err.errno != errno.ENOENT:
361 367 raise
362 368 # File doesn't exist, so the current state is empty
363 369 st = b''
364 370
365 371 l = len(st)
366 372 if l == self._nodelen * 2:
367 373 self._parents = (
368 374 st[: self._nodelen],
369 375 st[self._nodelen : 2 * self._nodelen],
370 376 )
371 377 elif l == 0:
372 378 self._parents = (
373 379 self._nodeconstants.nullid,
374 380 self._nodeconstants.nullid,
375 381 )
376 382 else:
377 383 raise error.Abort(
378 384 _(b'working directory state appears damaged!')
379 385 )
380 386
381 387 return self._parents
382 388
383 389 def setparents(self, p1, p2, fold_p2=False):
384 390 self._parents = (p1, p2)
385 391 self._dirtyparents = True
386 392 copies = {}
387 393 if fold_p2:
388 394 for f, s in pycompat.iteritems(self._map):
389 395 # Discard "merged" markers when moving away from a merge state
390 396 if s.merged or s.from_p2:
391 397 source = self.copymap.pop(f, None)
392 398 if source:
393 399 copies[f] = source
394 400 s.drop_merge_data()
395 401 return copies
396 402
397 403 def read(self):
398 404 # ignore HG_PENDING because identity is used only for writing
399 405 self.identity = util.filestat.frompath(
400 406 self._opener.join(self._filename)
401 407 )
402 408
403 409 try:
404 410 fp = self._opendirstatefile()
405 411 try:
406 412 st = fp.read()
407 413 finally:
408 414 fp.close()
409 415 except IOError as err:
410 416 if err.errno != errno.ENOENT:
411 417 raise
412 418 return
413 419 if not st:
414 420 return
415 421
416 422 if util.safehasattr(parsers, b'dict_new_presized'):
417 423 # Make an estimate of the number of files in the dirstate based on
418 424 # its size. This trades wasting some memory for avoiding costly
419 425 # resizes. Each entry have a prefix of 17 bytes followed by one or
420 426 # two path names. Studies on various large-scale real-world repositories
421 427 # found 54 bytes a reasonable upper limit for the average path names.
422 428 # Copy entries are ignored for the sake of this estimate.
423 429 self._map = parsers.dict_new_presized(len(st) // 71)
424 430
425 431 # Python's garbage collector triggers a GC each time a certain number
426 432 # of container objects (the number being defined by
427 433 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
428 434 # for each file in the dirstate. The C version then immediately marks
429 435 # them as not to be tracked by the collector. However, this has no
430 436 # effect on when GCs are triggered, only on what objects the GC looks
431 437 # into. This means that O(number of files) GCs are unavoidable.
432 438 # Depending on when in the process's lifetime the dirstate is parsed,
433 439 # this can get very expensive. As a workaround, disable GC while
434 440 # parsing the dirstate.
435 441 #
436 442 # (we cannot decorate the function directly since it is in a C module)
437 443 parse_dirstate = util.nogc(parsers.parse_dirstate)
438 444 p = parse_dirstate(self._map, self.copymap, st)
439 445 if not self._dirtyparents:
440 446 self.setparents(*p)
441 447
442 448 # Avoid excess attribute lookups by fast pathing certain checks
443 449 self.__contains__ = self._map.__contains__
444 450 self.__getitem__ = self._map.__getitem__
445 451 self.get = self._map.get
446 452
447 453 def write(self, _tr, st, now):
448 454 st.write(
449 455 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
450 456 )
451 457 st.close()
452 458 self._dirtyparents = False
453 459
454 460 @propertycache
455 461 def identity(self):
456 462 self._map
457 463 return self.identity
458 464
459 465 @propertycache
460 466 def dirfoldmap(self):
461 467 f = {}
462 468 normcase = util.normcase
463 469 for name in self._dirs:
464 470 f[normcase(name)] = name
465 471 return f
466 472
467 473
468 474 if rustmod is not None:
469 475
470 476 class dirstatemap(_dirstatemapcommon):
471 477 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
472 self._use_dirstate_v2 = use_dirstate_v2
473 self._nodeconstants = nodeconstants
474 self._ui = ui
475 self._opener = opener
476 self._root = root
477 self._filename = b'dirstate'
478 self._nodelen = 20 # Also update Rust code when changing this!
479 self._parents = None
480 self._dirtyparents = False
478 super(dirstatemap, self).__init__(
479 ui, opener, root, nodeconstants, use_dirstate_v2
480 )
481 481 self._docket = None
482 482
483 # for consistent view between _pl() and _read() invocations
484 self._pendingmode = None
485
486 483 def addfile(
487 484 self,
488 485 f,
489 486 mode=0,
490 487 size=None,
491 488 mtime=None,
492 489 added=False,
493 490 merged=False,
494 491 from_p2=False,
495 492 possibly_dirty=False,
496 493 ):
497 494 if added:
498 495 assert not possibly_dirty
499 496 assert not from_p2
500 497 item = DirstateItem.new_added()
501 498 elif merged:
502 499 assert not possibly_dirty
503 500 assert not from_p2
504 501 item = DirstateItem.new_merged()
505 502 elif from_p2:
506 503 assert not possibly_dirty
507 504 item = DirstateItem.new_from_p2()
508 505 elif possibly_dirty:
509 506 item = DirstateItem.new_possibly_dirty()
510 507 else:
511 508 assert size is not None
512 509 assert mtime is not None
513 510 size = size & rangemask
514 511 mtime = mtime & rangemask
515 512 item = DirstateItem.new_normal(mode, size, mtime)
516 513 self._rustmap.addfile(f, item)
517 514 if added:
518 515 self.copymap.pop(f, None)
519 516
520 517 def reset_state(
521 518 self,
522 519 filename,
523 520 wc_tracked=False,
524 521 p1_tracked=False,
525 522 p2_tracked=False,
526 523 merged=False,
527 524 clean_p1=False,
528 525 clean_p2=False,
529 526 possibly_dirty=False,
530 527 parentfiledata=None,
531 528 ):
532 529 """Set a entry to a given state, disregarding all previous state
533 530
534 531 This is to be used by the part of the dirstate API dedicated to
535 532 adjusting the dirstate after a update/merge.
536 533
537 534 note: calling this might result to no entry existing at all if the
538 535 dirstate map does not see any point at having one for this file
539 536 anymore.
540 537 """
541 538 if merged and (clean_p1 or clean_p2):
542 539 msg = (
543 540 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
544 541 )
545 542 raise error.ProgrammingError(msg)
546 543 # copy information are now outdated
547 544 # (maybe new information should be in directly passed to this function)
548 545 self.copymap.pop(filename, None)
549 546
550 547 if not (p1_tracked or p2_tracked or wc_tracked):
551 548 self._rustmap.drop_item_and_copy_source(filename)
552 549 elif merged:
553 550 # XXX might be merged and removed ?
554 551 entry = self.get(filename)
555 552 if entry is not None and entry.tracked:
556 553 # XXX mostly replicate dirstate.other parent. We should get
557 554 # the higher layer to pass us more reliable data where `merged`
558 555 # actually mean merged. Dropping the else clause will show
559 556 # failure in `test-graft.t`
560 557 self.addfile(filename, merged=True)
561 558 else:
562 559 self.addfile(filename, from_p2=True)
563 560 elif not (p1_tracked or p2_tracked) and wc_tracked:
564 561 self.addfile(
565 562 filename, added=True, possibly_dirty=possibly_dirty
566 563 )
567 564 elif (p1_tracked or p2_tracked) and not wc_tracked:
568 565 # XXX might be merged and removed ?
569 566 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
570 567 elif clean_p2 and wc_tracked:
571 568 if p1_tracked or self.get(filename) is not None:
572 569 # XXX the `self.get` call is catching some case in
573 570 # `test-merge-remove.t` where the file is tracked in p1, the
574 571 # p1_tracked argument is False.
575 572 #
576 573 # In addition, this seems to be a case where the file is marked
577 574 # as merged without actually being the result of a merge
578 575 # action. So thing are not ideal here.
579 576 self.addfile(filename, merged=True)
580 577 else:
581 578 self.addfile(filename, from_p2=True)
582 579 elif not p1_tracked and p2_tracked and wc_tracked:
583 580 self.addfile(
584 581 filename, from_p2=True, possibly_dirty=possibly_dirty
585 582 )
586 583 elif possibly_dirty:
587 584 self.addfile(filename, possibly_dirty=possibly_dirty)
588 585 elif wc_tracked:
589 586 # this is a "normal" file
590 587 if parentfiledata is None:
591 588 msg = b'failed to pass parentfiledata for a normal file: %s'
592 589 msg %= filename
593 590 raise error.ProgrammingError(msg)
594 591 mode, size, mtime = parentfiledata
595 592 self.addfile(filename, mode=mode, size=size, mtime=mtime)
596 593 else:
597 594 assert False, 'unreachable'
598 595
599 596 def set_tracked(self, filename):
600 597 new = False
601 598 entry = self.get(filename)
602 599 if entry is None:
603 600 self.addfile(filename, added=True)
604 601 new = True
605 602 elif not entry.tracked:
606 603 entry.set_tracked()
607 604 self._rustmap.set_dirstate_item(filename, entry)
608 605 new = True
609 606 else:
610 607 # XXX This is probably overkill for more case, but we need this to
611 608 # fully replace the `normallookup` call with `set_tracked` one.
612 609 # Consider smoothing this in the future.
613 610 self.set_possibly_dirty(filename)
614 611 return new
615 612
616 613 def set_untracked(self, f):
617 614 """Mark a file as no longer tracked in the dirstate map"""
618 615 # in merge is only trigger more logic, so it "fine" to pass it.
619 616 #
620 617 # the inner rust dirstate map code need to be adjusted once the API
621 618 # for dirstate/dirstatemap/DirstateItem is a bit more settled
622 619 entry = self.get(f)
623 620 if entry is None:
624 621 return False
625 622 else:
626 623 if entry.added:
627 624 self._rustmap.drop_item_and_copy_source(f)
628 625 else:
629 626 self._rustmap.removefile(f, in_merge=True)
630 627 return True
631 628
632 629 def removefile(self, *args, **kwargs):
633 630 return self._rustmap.removefile(*args, **kwargs)
634 631
635 632 def get(self, *args, **kwargs):
636 633 return self._rustmap.get(*args, **kwargs)
637 634
638 635 @property
639 636 def copymap(self):
640 637 return self._rustmap.copymap()
641 638
642 639 def debug_iter(self, all):
643 640 """
644 641 Return an iterator of (filename, state, mode, size, mtime) tuples
645 642
646 643 `all`: also include with `state == b' '` dirstate tree nodes that
647 644 don't have an associated `DirstateItem`.
648 645
649 646 """
650 647 return self._rustmap.debug_iter(all)
651 648
652 649 def preload(self):
653 650 self._rustmap
654 651
655 652 def clear(self):
656 653 self._rustmap.clear()
657 654 self.setparents(
658 655 self._nodeconstants.nullid, self._nodeconstants.nullid
659 656 )
660 657 util.clearcachedproperty(self, b"_dirs")
661 658 util.clearcachedproperty(self, b"_alldirs")
662 659 util.clearcachedproperty(self, b"dirfoldmap")
663 660
664 661 def items(self):
665 662 return self._rustmap.items()
666 663
667 664 def keys(self):
668 665 return iter(self._rustmap)
669 666
670 667 def __contains__(self, key):
671 668 return key in self._rustmap
672 669
673 670 def __getitem__(self, item):
674 671 return self._rustmap[item]
675 672
676 673 def __len__(self):
677 674 return len(self._rustmap)
678 675
679 676 def __iter__(self):
680 677 return iter(self._rustmap)
681 678
682 679 # forward for python2,3 compat
683 680 iteritems = items
684 681
685 682 def _opendirstatefile(self):
686 683 fp, mode = txnutil.trypending(
687 684 self._root, self._opener, self._filename
688 685 )
689 686 if self._pendingmode is not None and self._pendingmode != mode:
690 687 fp.close()
691 688 raise error.Abort(
692 689 _(b'working directory state may be changed parallelly')
693 690 )
694 691 self._pendingmode = mode
695 692 return fp
696 693
697 694 def _readdirstatefile(self, size=-1):
698 695 try:
699 696 with self._opendirstatefile() as fp:
700 697 return fp.read(size)
701 698 except IOError as err:
702 699 if err.errno != errno.ENOENT:
703 700 raise
704 701 # File doesn't exist, so the current state is empty
705 702 return b''
706 703
707 704 def setparents(self, p1, p2, fold_p2=False):
708 705 self._parents = (p1, p2)
709 706 self._dirtyparents = True
710 707 copies = {}
711 708 if fold_p2:
712 709 # Collect into an intermediate list to avoid a `RuntimeError`
713 710 # exception due to mutation during iteration.
714 711 # TODO: move this the whole loop to Rust where `iter_mut`
715 712 # enables in-place mutation of elements of a collection while
716 713 # iterating it, without mutating the collection itself.
717 714 candidatefiles = [
718 715 (f, s)
719 716 for f, s in self._rustmap.items()
720 717 if s.merged or s.from_p2
721 718 ]
722 719 for f, s in candidatefiles:
723 720 # Discard "merged" markers when moving away from a merge state
724 721 if s.merged:
725 722 source = self.copymap.get(f)
726 723 if source:
727 724 copies[f] = source
728 725 self.reset_state(
729 726 f,
730 727 wc_tracked=True,
731 728 p1_tracked=True,
732 729 possibly_dirty=True,
733 730 )
734 731 # Also fix up otherparent markers
735 732 elif s.from_p2:
736 733 source = self.copymap.get(f)
737 734 if source:
738 735 copies[f] = source
739 736 self.reset_state(
740 737 f,
741 738 p1_tracked=False,
742 739 wc_tracked=True,
743 740 )
744 741 return copies
745 742
746 743 def parents(self):
747 744 if not self._parents:
748 745 if self._use_dirstate_v2:
749 746 self._parents = self.docket.parents
750 747 else:
751 748 read_len = self._nodelen * 2
752 749 st = self._readdirstatefile(read_len)
753 750 l = len(st)
754 751 if l == read_len:
755 752 self._parents = (
756 753 st[: self._nodelen],
757 754 st[self._nodelen : 2 * self._nodelen],
758 755 )
759 756 elif l == 0:
760 757 self._parents = (
761 758 self._nodeconstants.nullid,
762 759 self._nodeconstants.nullid,
763 760 )
764 761 else:
765 762 raise error.Abort(
766 763 _(b'working directory state appears damaged!')
767 764 )
768 765
769 766 return self._parents
770 767
771 768 @property
772 769 def docket(self):
773 770 if not self._docket:
774 771 if not self._use_dirstate_v2:
775 772 raise error.ProgrammingError(
776 773 b'dirstate only has a docket in v2 format'
777 774 )
778 775 self._docket = docketmod.DirstateDocket.parse(
779 776 self._readdirstatefile(), self._nodeconstants
780 777 )
781 778 return self._docket
782 779
783 780 @propertycache
784 781 def _rustmap(self):
785 782 """
786 783 Fills the Dirstatemap when called.
787 784 """
788 785 # ignore HG_PENDING because identity is used only for writing
789 786 self.identity = util.filestat.frompath(
790 787 self._opener.join(self._filename)
791 788 )
792 789
793 790 if self._use_dirstate_v2:
794 791 if self.docket.uuid:
795 792 # TODO: use mmap when possible
796 793 data = self._opener.read(self.docket.data_filename())
797 794 else:
798 795 data = b''
799 796 self._rustmap = rustmod.DirstateMap.new_v2(
800 797 data, self.docket.data_size, self.docket.tree_metadata
801 798 )
802 799 parents = self.docket.parents
803 800 else:
804 801 self._rustmap, parents = rustmod.DirstateMap.new_v1(
805 802 self._readdirstatefile()
806 803 )
807 804
808 805 if parents and not self._dirtyparents:
809 806 self.setparents(*parents)
810 807
811 808 self.__contains__ = self._rustmap.__contains__
812 809 self.__getitem__ = self._rustmap.__getitem__
813 810 self.get = self._rustmap.get
814 811 return self._rustmap
815 812
816 813 def write(self, tr, st, now):
817 814 if not self._use_dirstate_v2:
818 815 p1, p2 = self.parents()
819 816 packed = self._rustmap.write_v1(p1, p2, now)
820 817 st.write(packed)
821 818 st.close()
822 819 self._dirtyparents = False
823 820 return
824 821
825 822 # We can only append to an existing data file if there is one
826 823 can_append = self.docket.uuid is not None
827 824 packed, meta, append = self._rustmap.write_v2(now, can_append)
828 825 if append:
829 826 docket = self.docket
830 827 data_filename = docket.data_filename()
831 828 if tr:
832 829 tr.add(data_filename, docket.data_size)
833 830 with self._opener(data_filename, b'r+b') as fp:
834 831 fp.seek(docket.data_size)
835 832 assert fp.tell() == docket.data_size
836 833 written = fp.write(packed)
837 834 if written is not None: # py2 may return None
838 835 assert written == len(packed), (written, len(packed))
839 836 docket.data_size += len(packed)
840 837 docket.parents = self.parents()
841 838 docket.tree_metadata = meta
842 839 st.write(docket.serialize())
843 840 st.close()
844 841 else:
845 842 old_docket = self.docket
846 843 new_docket = docketmod.DirstateDocket.with_new_uuid(
847 844 self.parents(), len(packed), meta
848 845 )
849 846 data_filename = new_docket.data_filename()
850 847 if tr:
851 848 tr.add(data_filename, 0)
852 849 self._opener.write(data_filename, packed)
853 850 # Write the new docket after the new data file has been
854 851 # written. Because `st` was opened with `atomictemp=True`,
855 852 # the actual `.hg/dirstate` file is only affected on close.
856 853 st.write(new_docket.serialize())
857 854 st.close()
858 855 # Remove the old data file after the new docket pointing to
859 856 # the new data file was written.
860 857 if old_docket.uuid:
861 858 data_filename = old_docket.data_filename()
862 859 unlink = lambda _tr=None: self._opener.unlink(data_filename)
863 860 if tr:
864 861 category = b"dirstate-v2-clean-" + old_docket.uuid
865 862 tr.addpostclose(category, unlink)
866 863 else:
867 864 unlink()
868 865 self._docket = new_docket
869 866 # Reload from the newly-written file
870 867 util.clearcachedproperty(self, b"_rustmap")
871 868 self._dirtyparents = False
872 869
873 870 @propertycache
874 871 def filefoldmap(self):
875 872 """Returns a dictionary mapping normalized case paths to their
876 873 non-normalized versions.
877 874 """
878 875 return self._rustmap.filefoldmapasdict()
879 876
880 877 def hastrackeddir(self, d):
881 878 return self._rustmap.hastrackeddir(d)
882 879
883 880 def hasdir(self, d):
884 881 return self._rustmap.hasdir(d)
885 882
886 883 @propertycache
887 884 def identity(self):
888 885 self._rustmap
889 886 return self.identity
890 887
891 888 @propertycache
892 889 def dirfoldmap(self):
893 890 f = {}
894 891 normcase = util.normcase
895 892 for name in self._rustmap.tracked_dirs():
896 893 f[normcase(name)] = name
897 894 return f
898 895
899 896 def set_possibly_dirty(self, filename):
900 897 """record that the current state of the file on disk is unknown"""
901 898 entry = self[filename]
902 899 entry.set_possibly_dirty()
903 900 self._rustmap.set_dirstate_item(filename, entry)
904 901
905 902 def set_clean(self, filename, mode, size, mtime):
906 903 """mark a file as back to a clean state"""
907 904 entry = self[filename]
908 905 mtime = mtime & rangemask
909 906 size = size & rangemask
910 907 entry.set_clean(mode, size, mtime)
911 908 self._rustmap.set_dirstate_item(filename, entry)
912 909 self._rustmap.copymap().pop(filename, None)
913 910
914 911 def __setitem__(self, key, value):
915 912 assert isinstance(value, DirstateItem)
916 913 self._rustmap.set_dirstate_item(key, value)
General Comments 0
You need to be logged in to leave comments. Login now