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