##// END OF EJS Templates
dirstate-v2: complain early on docket name collision...
Arseniy Alekseyev -
r50992:ca9d65d6 stable
parent child Browse files
Show More
@@ -1,686 +1,688 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
7 7 from .i18n import _
8 8
9 9 from . import (
10 10 error,
11 11 pathutil,
12 12 policy,
13 13 txnutil,
14 14 util,
15 15 )
16 16
17 17 from .dirstateutils import (
18 18 docket as docketmod,
19 19 v2,
20 20 )
21 21
22 22 parsers = policy.importmod('parsers')
23 23 rustmod = policy.importrust('dirstate')
24 24
25 25 propertycache = util.propertycache
26 26
27 27 if rustmod is None:
28 28 DirstateItem = parsers.DirstateItem
29 29 else:
30 30 DirstateItem = rustmod.DirstateItem
31 31
32 32 rangemask = 0x7FFFFFFF
33 33
34 34
35 35 class _dirstatemapcommon:
36 36 """
37 37 Methods that are identical for both implementations of the dirstatemap
38 38 class, with and without Rust extensions enabled.
39 39 """
40 40
41 41 # please pytype
42 42
43 43 _map = None
44 44 copymap = None
45 45
46 46 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
47 47 self._use_dirstate_v2 = use_dirstate_v2
48 48 self._nodeconstants = nodeconstants
49 49 self._ui = ui
50 50 self._opener = opener
51 51 self._root = root
52 52 self._filename = b'dirstate'
53 53 self._nodelen = 20 # Also update Rust code when changing this!
54 54 self._parents = None
55 55 self._dirtyparents = False
56 56 self._docket = None
57 57
58 58 # for consistent view between _pl() and _read() invocations
59 59 self._pendingmode = None
60 60
61 61 def preload(self):
62 62 """Loads the underlying data, if it's not already loaded"""
63 63 self._map
64 64
65 65 def get(self, key, default=None):
66 66 return self._map.get(key, default)
67 67
68 68 def __len__(self):
69 69 return len(self._map)
70 70
71 71 def __iter__(self):
72 72 return iter(self._map)
73 73
74 74 def __contains__(self, key):
75 75 return key in self._map
76 76
77 77 def __getitem__(self, item):
78 78 return self._map[item]
79 79
80 80 ### disk interaction
81 81
82 82 def _opendirstatefile(self):
83 83 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
84 84 if self._pendingmode is not None and self._pendingmode != mode:
85 85 fp.close()
86 86 raise error.Abort(
87 87 _(b'working directory state may be changed parallelly')
88 88 )
89 89 self._pendingmode = mode
90 90 return fp
91 91
92 92 def _readdirstatefile(self, size=-1):
93 93 try:
94 94 with self._opendirstatefile() as fp:
95 95 return fp.read(size)
96 96 except FileNotFoundError:
97 97 # File doesn't exist, so the current state is empty
98 98 return b''
99 99
100 100 @property
101 101 def docket(self):
102 102 if not self._docket:
103 103 if not self._use_dirstate_v2:
104 104 raise error.ProgrammingError(
105 105 b'dirstate only has a docket in v2 format'
106 106 )
107 107 self._docket = docketmod.DirstateDocket.parse(
108 108 self._readdirstatefile(), self._nodeconstants
109 109 )
110 110 return self._docket
111 111
112 112 def write_v2_no_append(self, tr, st, meta, packed):
113 113 old_docket = self.docket
114 114 new_docket = docketmod.DirstateDocket.with_new_uuid(
115 115 self.parents(), len(packed), meta
116 116 )
117 if old_docket.uuid == new_docket.uuid:
118 raise error.ProgrammingError(b'dirstate docket name collision')
117 119 data_filename = new_docket.data_filename()
118 120 self._opener.write(data_filename, packed)
119 121 # Write the new docket after the new data file has been
120 122 # written. Because `st` was opened with `atomictemp=True`,
121 123 # the actual `.hg/dirstate` file is only affected on close.
122 124 st.write(new_docket.serialize())
123 125 st.close()
124 126 # Remove the old data file after the new docket pointing to
125 127 # the new data file was written.
126 128 if old_docket.uuid:
127 129 data_filename = old_docket.data_filename()
128 130 unlink = lambda _tr=None: self._opener.unlink(data_filename)
129 131 if tr:
130 132 category = b"dirstate-v2-clean-" + old_docket.uuid
131 133 tr.addpostclose(category, unlink)
132 134 else:
133 135 unlink()
134 136 self._docket = new_docket
135 137
136 138 ### reading/setting parents
137 139
138 140 def parents(self):
139 141 if not self._parents:
140 142 if self._use_dirstate_v2:
141 143 self._parents = self.docket.parents
142 144 else:
143 145 read_len = self._nodelen * 2
144 146 st = self._readdirstatefile(read_len)
145 147 l = len(st)
146 148 if l == read_len:
147 149 self._parents = (
148 150 st[: self._nodelen],
149 151 st[self._nodelen : 2 * self._nodelen],
150 152 )
151 153 elif l == 0:
152 154 self._parents = (
153 155 self._nodeconstants.nullid,
154 156 self._nodeconstants.nullid,
155 157 )
156 158 else:
157 159 raise error.Abort(
158 160 _(b'working directory state appears damaged!')
159 161 )
160 162
161 163 return self._parents
162 164
163 165
164 166 class dirstatemap(_dirstatemapcommon):
165 167 """Map encapsulating the dirstate's contents.
166 168
167 169 The dirstate contains the following state:
168 170
169 171 - `identity` is the identity of the dirstate file, which can be used to
170 172 detect when changes have occurred to the dirstate file.
171 173
172 174 - `parents` is a pair containing the parents of the working copy. The
173 175 parents are updated by calling `setparents`.
174 176
175 177 - the state map maps filenames to tuples of (state, mode, size, mtime),
176 178 where state is a single character representing 'normal', 'added',
177 179 'removed', or 'merged'. It is read by treating the dirstate as a
178 180 dict. File state is updated by calling various methods (see each
179 181 documentation for details):
180 182
181 183 - `reset_state`,
182 184 - `set_tracked`
183 185 - `set_untracked`
184 186 - `set_clean`
185 187 - `set_possibly_dirty`
186 188
187 189 - `copymap` maps destination filenames to their source filename.
188 190
189 191 The dirstate also provides the following views onto the state:
190 192
191 193 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
192 194 form that they appear as in the dirstate.
193 195
194 196 - `dirfoldmap` is a dict mapping normalized directory names to the
195 197 denormalized form that they appear as in the dirstate.
196 198 """
197 199
198 200 ### Core data storage and access
199 201
200 202 @propertycache
201 203 def _map(self):
202 204 self._map = {}
203 205 self.read()
204 206 return self._map
205 207
206 208 @propertycache
207 209 def copymap(self):
208 210 self.copymap = {}
209 211 self._map
210 212 return self.copymap
211 213
212 214 def clear(self):
213 215 self._map.clear()
214 216 self.copymap.clear()
215 217 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
216 218 util.clearcachedproperty(self, b"_dirs")
217 219 util.clearcachedproperty(self, b"_alldirs")
218 220 util.clearcachedproperty(self, b"filefoldmap")
219 221 util.clearcachedproperty(self, b"dirfoldmap")
220 222
221 223 def items(self):
222 224 return self._map.items()
223 225
224 226 # forward for python2,3 compat
225 227 iteritems = items
226 228
227 229 def debug_iter(self, all):
228 230 """
229 231 Return an iterator of (filename, state, mode, size, mtime) tuples
230 232
231 233 `all` is unused when Rust is not enabled
232 234 """
233 235 for (filename, item) in self.items():
234 236 yield (filename, item.state, item.mode, item.size, item.mtime)
235 237
236 238 def keys(self):
237 239 return self._map.keys()
238 240
239 241 ### reading/setting parents
240 242
241 243 def setparents(self, p1, p2, fold_p2=False):
242 244 self._parents = (p1, p2)
243 245 self._dirtyparents = True
244 246 copies = {}
245 247 if fold_p2:
246 248 for f, s in self._map.items():
247 249 # Discard "merged" markers when moving away from a merge state
248 250 if s.p2_info:
249 251 source = self.copymap.pop(f, None)
250 252 if source:
251 253 copies[f] = source
252 254 s.drop_merge_data()
253 255 return copies
254 256
255 257 ### disk interaction
256 258
257 259 def read(self):
258 260 # ignore HG_PENDING because identity is used only for writing
259 261 self.identity = util.filestat.frompath(
260 262 self._opener.join(self._filename)
261 263 )
262 264
263 265 if self._use_dirstate_v2:
264 266 if not self.docket.uuid:
265 267 return
266 268 st = self._opener.read(self.docket.data_filename())
267 269 else:
268 270 st = self._readdirstatefile()
269 271
270 272 if not st:
271 273 return
272 274
273 275 # TODO: adjust this estimate for dirstate-v2
274 276 if util.safehasattr(parsers, b'dict_new_presized'):
275 277 # Make an estimate of the number of files in the dirstate based on
276 278 # its size. This trades wasting some memory for avoiding costly
277 279 # resizes. Each entry have a prefix of 17 bytes followed by one or
278 280 # two path names. Studies on various large-scale real-world repositories
279 281 # found 54 bytes a reasonable upper limit for the average path names.
280 282 # Copy entries are ignored for the sake of this estimate.
281 283 self._map = parsers.dict_new_presized(len(st) // 71)
282 284
283 285 # Python's garbage collector triggers a GC each time a certain number
284 286 # of container objects (the number being defined by
285 287 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
286 288 # for each file in the dirstate. The C version then immediately marks
287 289 # them as not to be tracked by the collector. However, this has no
288 290 # effect on when GCs are triggered, only on what objects the GC looks
289 291 # into. This means that O(number of files) GCs are unavoidable.
290 292 # Depending on when in the process's lifetime the dirstate is parsed,
291 293 # this can get very expensive. As a workaround, disable GC while
292 294 # parsing the dirstate.
293 295 #
294 296 # (we cannot decorate the function directly since it is in a C module)
295 297 if self._use_dirstate_v2:
296 298 p = self.docket.parents
297 299 meta = self.docket.tree_metadata
298 300 parse_dirstate = util.nogc(v2.parse_dirstate)
299 301 parse_dirstate(self._map, self.copymap, st, meta)
300 302 else:
301 303 parse_dirstate = util.nogc(parsers.parse_dirstate)
302 304 p = parse_dirstate(self._map, self.copymap, st)
303 305 if not self._dirtyparents:
304 306 self.setparents(*p)
305 307
306 308 # Avoid excess attribute lookups by fast pathing certain checks
307 309 self.__contains__ = self._map.__contains__
308 310 self.__getitem__ = self._map.__getitem__
309 311 self.get = self._map.get
310 312
311 313 def write(self, tr, st):
312 314 if self._use_dirstate_v2:
313 315 packed, meta = v2.pack_dirstate(self._map, self.copymap)
314 316 self.write_v2_no_append(tr, st, meta, packed)
315 317 else:
316 318 packed = parsers.pack_dirstate(
317 319 self._map, self.copymap, self.parents()
318 320 )
319 321 st.write(packed)
320 322 st.close()
321 323 self._dirtyparents = False
322 324
323 325 @propertycache
324 326 def identity(self):
325 327 self._map
326 328 return self.identity
327 329
328 330 ### code related to maintaining and accessing "extra" property
329 331 # (e.g. "has_dir")
330 332
331 333 def _dirs_incr(self, filename, old_entry=None):
332 334 """increment the dirstate counter if applicable"""
333 335 if (
334 336 old_entry is None or old_entry.removed
335 337 ) and "_dirs" in self.__dict__:
336 338 self._dirs.addpath(filename)
337 339 if old_entry is None and "_alldirs" in self.__dict__:
338 340 self._alldirs.addpath(filename)
339 341
340 342 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
341 343 """decrement the dirstate counter if applicable"""
342 344 if old_entry is not None:
343 345 if "_dirs" in self.__dict__ and not old_entry.removed:
344 346 self._dirs.delpath(filename)
345 347 if "_alldirs" in self.__dict__ and not remove_variant:
346 348 self._alldirs.delpath(filename)
347 349 elif remove_variant and "_alldirs" in self.__dict__:
348 350 self._alldirs.addpath(filename)
349 351 if "filefoldmap" in self.__dict__:
350 352 normed = util.normcase(filename)
351 353 self.filefoldmap.pop(normed, None)
352 354
353 355 @propertycache
354 356 def filefoldmap(self):
355 357 """Returns a dictionary mapping normalized case paths to their
356 358 non-normalized versions.
357 359 """
358 360 try:
359 361 makefilefoldmap = parsers.make_file_foldmap
360 362 except AttributeError:
361 363 pass
362 364 else:
363 365 return makefilefoldmap(
364 366 self._map, util.normcasespec, util.normcasefallback
365 367 )
366 368
367 369 f = {}
368 370 normcase = util.normcase
369 371 for name, s in self._map.items():
370 372 if not s.removed:
371 373 f[normcase(name)] = name
372 374 f[b'.'] = b'.' # prevents useless util.fspath() invocation
373 375 return f
374 376
375 377 @propertycache
376 378 def dirfoldmap(self):
377 379 f = {}
378 380 normcase = util.normcase
379 381 for name in self._dirs:
380 382 f[normcase(name)] = name
381 383 return f
382 384
383 385 def hastrackeddir(self, d):
384 386 """
385 387 Returns True if the dirstate contains a tracked (not removed) file
386 388 in this directory.
387 389 """
388 390 return d in self._dirs
389 391
390 392 def hasdir(self, d):
391 393 """
392 394 Returns True if the dirstate contains a file (tracked or removed)
393 395 in this directory.
394 396 """
395 397 return d in self._alldirs
396 398
397 399 @propertycache
398 400 def _dirs(self):
399 401 return pathutil.dirs(self._map, only_tracked=True)
400 402
401 403 @propertycache
402 404 def _alldirs(self):
403 405 return pathutil.dirs(self._map)
404 406
405 407 ### code related to manipulation of entries and copy-sources
406 408
407 409 def reset_state(
408 410 self,
409 411 filename,
410 412 wc_tracked=False,
411 413 p1_tracked=False,
412 414 p2_info=False,
413 415 has_meaningful_mtime=True,
414 416 parentfiledata=None,
415 417 ):
416 418 """Set a entry to a given state, diregarding all previous state
417 419
418 420 This is to be used by the part of the dirstate API dedicated to
419 421 adjusting the dirstate after a update/merge.
420 422
421 423 note: calling this might result to no entry existing at all if the
422 424 dirstate map does not see any point at having one for this file
423 425 anymore.
424 426 """
425 427 # copy information are now outdated
426 428 # (maybe new information should be in directly passed to this function)
427 429 self.copymap.pop(filename, None)
428 430
429 431 if not (p1_tracked or p2_info or wc_tracked):
430 432 old_entry = self._map.get(filename)
431 433 self._drop_entry(filename)
432 434 self._dirs_decr(filename, old_entry=old_entry)
433 435 return
434 436
435 437 old_entry = self._map.get(filename)
436 438 self._dirs_incr(filename, old_entry)
437 439 entry = DirstateItem(
438 440 wc_tracked=wc_tracked,
439 441 p1_tracked=p1_tracked,
440 442 p2_info=p2_info,
441 443 has_meaningful_mtime=has_meaningful_mtime,
442 444 parentfiledata=parentfiledata,
443 445 )
444 446 self._map[filename] = entry
445 447
446 448 def set_tracked(self, filename):
447 449 new = False
448 450 entry = self.get(filename)
449 451 if entry is None:
450 452 self._dirs_incr(filename)
451 453 entry = DirstateItem(
452 454 wc_tracked=True,
453 455 )
454 456
455 457 self._map[filename] = entry
456 458 new = True
457 459 elif not entry.tracked:
458 460 self._dirs_incr(filename, entry)
459 461 entry.set_tracked()
460 462 self._refresh_entry(filename, entry)
461 463 new = True
462 464 else:
463 465 # XXX This is probably overkill for more case, but we need this to
464 466 # fully replace the `normallookup` call with `set_tracked` one.
465 467 # Consider smoothing this in the future.
466 468 entry.set_possibly_dirty()
467 469 self._refresh_entry(filename, entry)
468 470 return new
469 471
470 472 def set_untracked(self, f):
471 473 """Mark a file as no longer tracked in the dirstate map"""
472 474 entry = self.get(f)
473 475 if entry is None:
474 476 return False
475 477 else:
476 478 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
477 479 if not entry.p2_info:
478 480 self.copymap.pop(f, None)
479 481 entry.set_untracked()
480 482 self._refresh_entry(f, entry)
481 483 return True
482 484
483 485 def set_clean(self, filename, mode, size, mtime):
484 486 """mark a file as back to a clean state"""
485 487 entry = self[filename]
486 488 size = size & rangemask
487 489 entry.set_clean(mode, size, mtime)
488 490 self._refresh_entry(filename, entry)
489 491 self.copymap.pop(filename, None)
490 492
491 493 def set_possibly_dirty(self, filename):
492 494 """record that the current state of the file on disk is unknown"""
493 495 entry = self[filename]
494 496 entry.set_possibly_dirty()
495 497 self._refresh_entry(filename, entry)
496 498
497 499 def _refresh_entry(self, f, entry):
498 500 """record updated state of an entry"""
499 501 if not entry.any_tracked:
500 502 self._map.pop(f, None)
501 503
502 504 def _drop_entry(self, f):
503 505 """remove any entry for file f
504 506
505 507 This should also drop associated copy information
506 508
507 509 The fact we actually need to drop it is the responsability of the caller"""
508 510 self._map.pop(f, None)
509 511 self.copymap.pop(f, None)
510 512
511 513
512 514 if rustmod is not None:
513 515
514 516 class dirstatemap(_dirstatemapcommon):
515 517
516 518 ### Core data storage and access
517 519
518 520 @propertycache
519 521 def _map(self):
520 522 """
521 523 Fills the Dirstatemap when called.
522 524 """
523 525 # ignore HG_PENDING because identity is used only for writing
524 526 self.identity = util.filestat.frompath(
525 527 self._opener.join(self._filename)
526 528 )
527 529
528 530 if self._use_dirstate_v2:
529 531 if self.docket.uuid:
530 532 # TODO: use mmap when possible
531 533 data = self._opener.read(self.docket.data_filename())
532 534 else:
533 535 data = b''
534 536 self._map = rustmod.DirstateMap.new_v2(
535 537 data, self.docket.data_size, self.docket.tree_metadata
536 538 )
537 539 parents = self.docket.parents
538 540 else:
539 541 self._map, parents = rustmod.DirstateMap.new_v1(
540 542 self._readdirstatefile()
541 543 )
542 544
543 545 if parents and not self._dirtyparents:
544 546 self.setparents(*parents)
545 547
546 548 self.__contains__ = self._map.__contains__
547 549 self.__getitem__ = self._map.__getitem__
548 550 self.get = self._map.get
549 551 return self._map
550 552
551 553 @property
552 554 def copymap(self):
553 555 return self._map.copymap()
554 556
555 557 def debug_iter(self, all):
556 558 """
557 559 Return an iterator of (filename, state, mode, size, mtime) tuples
558 560
559 561 `all`: also include with `state == b' '` dirstate tree nodes that
560 562 don't have an associated `DirstateItem`.
561 563
562 564 """
563 565 return self._map.debug_iter(all)
564 566
565 567 def clear(self):
566 568 self._map.clear()
567 569 self.setparents(
568 570 self._nodeconstants.nullid, self._nodeconstants.nullid
569 571 )
570 572 util.clearcachedproperty(self, b"_dirs")
571 573 util.clearcachedproperty(self, b"_alldirs")
572 574 util.clearcachedproperty(self, b"dirfoldmap")
573 575
574 576 def items(self):
575 577 return self._map.items()
576 578
577 579 # forward for python2,3 compat
578 580 iteritems = items
579 581
580 582 def keys(self):
581 583 return iter(self._map)
582 584
583 585 ### reading/setting parents
584 586
585 587 def setparents(self, p1, p2, fold_p2=False):
586 588 self._parents = (p1, p2)
587 589 self._dirtyparents = True
588 590 copies = {}
589 591 if fold_p2:
590 592 copies = self._map.setparents_fixup()
591 593 return copies
592 594
593 595 ### disk interaction
594 596
595 597 @propertycache
596 598 def identity(self):
597 599 self._map
598 600 return self.identity
599 601
600 602 def write(self, tr, st):
601 603 if not self._use_dirstate_v2:
602 604 p1, p2 = self.parents()
603 605 packed = self._map.write_v1(p1, p2)
604 606 st.write(packed)
605 607 st.close()
606 608 self._dirtyparents = False
607 609 return
608 610
609 611 # We can only append to an existing data file if there is one
610 612 can_append = self.docket.uuid is not None
611 613 packed, meta, append = self._map.write_v2(can_append)
612 614 if append:
613 615 docket = self.docket
614 616 data_filename = docket.data_filename()
615 617 with self._opener(data_filename, b'r+b') as fp:
616 618 fp.seek(docket.data_size)
617 619 assert fp.tell() == docket.data_size
618 620 written = fp.write(packed)
619 621 if written is not None: # py2 may return None
620 622 assert written == len(packed), (written, len(packed))
621 623 docket.data_size += len(packed)
622 624 docket.parents = self.parents()
623 625 docket.tree_metadata = meta
624 626 st.write(docket.serialize())
625 627 st.close()
626 628 else:
627 629 self.write_v2_no_append(tr, st, meta, packed)
628 630 # Reload from the newly-written file
629 631 util.clearcachedproperty(self, b"_map")
630 632 self._dirtyparents = False
631 633
632 634 ### code related to maintaining and accessing "extra" property
633 635 # (e.g. "has_dir")
634 636
635 637 @propertycache
636 638 def filefoldmap(self):
637 639 """Returns a dictionary mapping normalized case paths to their
638 640 non-normalized versions.
639 641 """
640 642 return self._map.filefoldmapasdict()
641 643
642 644 def hastrackeddir(self, d):
643 645 return self._map.hastrackeddir(d)
644 646
645 647 def hasdir(self, d):
646 648 return self._map.hasdir(d)
647 649
648 650 @propertycache
649 651 def dirfoldmap(self):
650 652 f = {}
651 653 normcase = util.normcase
652 654 for name in self._map.tracked_dirs():
653 655 f[normcase(name)] = name
654 656 return f
655 657
656 658 ### code related to manipulation of entries and copy-sources
657 659
658 660 def set_tracked(self, f):
659 661 return self._map.set_tracked(f)
660 662
661 663 def set_untracked(self, f):
662 664 return self._map.set_untracked(f)
663 665
664 666 def set_clean(self, filename, mode, size, mtime):
665 667 self._map.set_clean(filename, mode, size, mtime)
666 668
667 669 def set_possibly_dirty(self, f):
668 670 self._map.set_possibly_dirty(f)
669 671
670 672 def reset_state(
671 673 self,
672 674 filename,
673 675 wc_tracked=False,
674 676 p1_tracked=False,
675 677 p2_info=False,
676 678 has_meaningful_mtime=True,
677 679 parentfiledata=None,
678 680 ):
679 681 return self._map.reset_state(
680 682 filename,
681 683 wc_tracked,
682 684 p1_tracked,
683 685 p2_info,
684 686 has_meaningful_mtime,
685 687 parentfiledata,
686 688 )
General Comments 0
You need to be logged in to leave comments. Login now