##// END OF EJS Templates
dirstate-item: replace call to new_added...
marmoute -
r48968:d342815f default
parent child Browse files
Show More
@@ -1,767 +1,767 b''
1 1 # parsers.py - Python implementation of parsers.c
2 2 #
3 3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import struct
11 11 import zlib
12 12
13 13 from ..node import (
14 14 nullrev,
15 15 sha1nodeconstants,
16 16 )
17 17 from ..thirdparty import attr
18 18 from .. import (
19 19 error,
20 20 pycompat,
21 21 revlogutils,
22 22 util,
23 23 )
24 24
25 25 from ..revlogutils import nodemap as nodemaputil
26 26 from ..revlogutils import constants as revlog_constants
27 27
28 28 stringio = pycompat.bytesio
29 29
30 30
31 31 _pack = struct.pack
32 32 _unpack = struct.unpack
33 33 _compress = zlib.compress
34 34 _decompress = zlib.decompress
35 35
36 36
37 37 # a special value used internally for `size` if the file come from the other parent
38 38 FROM_P2 = -2
39 39
40 40 # a special value used internally for `size` if the file is modified/merged/added
41 41 NONNORMAL = -1
42 42
43 43 # a special value used internally for `time` if the time is ambigeous
44 44 AMBIGUOUS_TIME = -1
45 45
46 46
47 47 @attr.s(slots=True, init=False)
48 48 class DirstateItem(object):
49 49 """represent a dirstate entry
50 50
51 51 It hold multiple attributes
52 52
53 53 # about file tracking
54 54 - wc_tracked: is the file tracked by the working copy
55 55 - p1_tracked: is the file tracked in working copy first parent
56 56 - p2_info: the file has been involved in some merge operation. Either
57 57 because it was actually merged, or because the p2 version was
58 58 ahead, or because some renamed moved it there. In either case
59 59 `hg status` will want it displayed as modified.
60 60
61 61 # about the file state expected from p1 manifest:
62 62 - mode: the file mode in p1
63 63 - size: the file size in p1
64 64
65 65 These value can be set to None, which mean we don't have a meaningful value
66 66 to compare with. Either because we don't really care about them as there
67 67 `status` is known without having to look at the disk or because we don't
68 68 know these right now and a full comparison will be needed to find out if
69 69 the file is clean.
70 70
71 71 # about the file state on disk last time we saw it:
72 72 - mtime: the last known clean mtime for the file.
73 73
74 74 This value can be set to None if no cachable state exist. Either because we
75 75 do not care (see previous section) or because we could not cache something
76 76 yet.
77 77 """
78 78
79 79 _wc_tracked = attr.ib()
80 80 _p1_tracked = attr.ib()
81 81 _p2_info = attr.ib()
82 82 _mode = attr.ib()
83 83 _size = attr.ib()
84 84 _mtime = attr.ib()
85 85
86 86 def __init__(
87 87 self,
88 88 wc_tracked=False,
89 89 p1_tracked=False,
90 90 p2_info=False,
91 91 has_meaningful_data=True,
92 92 has_meaningful_mtime=True,
93 93 parentfiledata=None,
94 94 ):
95 95 self._wc_tracked = wc_tracked
96 96 self._p1_tracked = p1_tracked
97 97 self._p2_info = p2_info
98 98
99 99 self._mode = None
100 100 self._size = None
101 101 self._mtime = None
102 102 if parentfiledata is None:
103 103 has_meaningful_mtime = False
104 104 has_meaningful_data = False
105 105 if has_meaningful_data:
106 106 self._mode = parentfiledata[0]
107 107 self._size = parentfiledata[1]
108 108 if has_meaningful_mtime:
109 109 self._mtime = parentfiledata[2]
110 110
111 111 @classmethod
112 112 def new_added(cls):
113 113 """constructor to help legacy API to build a new "added" item
114 114
115 115 Should eventually be removed
116 116 """
117 117 return cls(wc_tracked=True)
118 118
119 119 @classmethod
120 120 def new_from_p2(cls):
121 121 """constructor to help legacy API to build a new "from_p2" item
122 122
123 123 Should eventually be removed
124 124 """
125 125 return cls(wc_tracked=True, p2_info=True)
126 126
127 127 @classmethod
128 128 def new_possibly_dirty(cls):
129 129 """constructor to help legacy API to build a new "possibly_dirty" item
130 130
131 131 Should eventually be removed
132 132 """
133 133 return cls(wc_tracked=True, p1_tracked=True)
134 134
135 135 @classmethod
136 136 def new_normal(cls, mode, size, mtime):
137 137 """constructor to help legacy API to build a new "normal" item
138 138
139 139 Should eventually be removed
140 140 """
141 141 assert size != FROM_P2
142 142 assert size != NONNORMAL
143 143 return cls(
144 144 wc_tracked=True,
145 145 p1_tracked=True,
146 146 parentfiledata=(mode, size, mtime),
147 147 )
148 148
149 149 @classmethod
150 150 def from_v1_data(cls, state, mode, size, mtime):
151 151 """Build a new DirstateItem object from V1 data
152 152
153 153 Since the dirstate-v1 format is frozen, the signature of this function
154 154 is not expected to change, unlike the __init__ one.
155 155 """
156 156 if state == b'm':
157 157 return cls(wc_tracked=True, p1_tracked=True, p2_info=True)
158 158 elif state == b'a':
159 return cls.new_added()
159 return cls(wc_tracked=True)
160 160 elif state == b'r':
161 161 if size == NONNORMAL:
162 162 p1_tracked = True
163 163 p2_info = True
164 164 elif size == FROM_P2:
165 165 p1_tracked = False
166 166 p2_info = True
167 167 else:
168 168 p1_tracked = True
169 169 p2_info = False
170 170 return cls(p1_tracked=p1_tracked, p2_info=p2_info)
171 171 elif state == b'n':
172 172 if size == FROM_P2:
173 173 return cls.new_from_p2()
174 174 elif size == NONNORMAL:
175 175 return cls.new_possibly_dirty()
176 176 elif mtime == AMBIGUOUS_TIME:
177 177 instance = cls.new_normal(mode, size, 42)
178 178 instance._mtime = None
179 179 return instance
180 180 else:
181 181 return cls.new_normal(mode, size, mtime)
182 182 else:
183 183 raise RuntimeError(b'unknown state: %s' % state)
184 184
185 185 def set_possibly_dirty(self):
186 186 """Mark a file as "possibly dirty"
187 187
188 188 This means the next status call will have to actually check its content
189 189 to make sure it is correct.
190 190 """
191 191 self._mtime = None
192 192
193 193 def set_clean(self, mode, size, mtime):
194 194 """mark a file as "clean" cancelling potential "possibly dirty call"
195 195
196 196 Note: this function is a descendant of `dirstate.normal` and is
197 197 currently expected to be call on "normal" entry only. There are not
198 198 reason for this to not change in the future as long as the ccode is
199 199 updated to preserve the proper state of the non-normal files.
200 200 """
201 201 self._wc_tracked = True
202 202 self._p1_tracked = True
203 203 self._mode = mode
204 204 self._size = size
205 205 self._mtime = mtime
206 206
207 207 def set_tracked(self):
208 208 """mark a file as tracked in the working copy
209 209
210 210 This will ultimately be called by command like `hg add`.
211 211 """
212 212 self._wc_tracked = True
213 213 # `set_tracked` is replacing various `normallookup` call. So we mark
214 214 # the files as needing lookup
215 215 #
216 216 # Consider dropping this in the future in favor of something less broad.
217 217 self._mtime = None
218 218
219 219 def set_untracked(self):
220 220 """mark a file as untracked in the working copy
221 221
222 222 This will ultimately be called by command like `hg remove`.
223 223 """
224 224 self._wc_tracked = False
225 225 self._mode = None
226 226 self._size = None
227 227 self._mtime = None
228 228
229 229 def drop_merge_data(self):
230 230 """remove all "merge-only" from a DirstateItem
231 231
232 232 This is to be call by the dirstatemap code when the second parent is dropped
233 233 """
234 234 if self._p2_info:
235 235 self._p2_info = False
236 236 self._mode = None
237 237 self._size = None
238 238 self._mtime = None
239 239
240 240 @property
241 241 def mode(self):
242 242 return self.v1_mode()
243 243
244 244 @property
245 245 def size(self):
246 246 return self.v1_size()
247 247
248 248 @property
249 249 def mtime(self):
250 250 return self.v1_mtime()
251 251
252 252 @property
253 253 def state(self):
254 254 """
255 255 States are:
256 256 n normal
257 257 m needs merging
258 258 r marked for removal
259 259 a marked for addition
260 260
261 261 XXX This "state" is a bit obscure and mostly a direct expression of the
262 262 dirstatev1 format. It would make sense to ultimately deprecate it in
263 263 favor of the more "semantic" attributes.
264 264 """
265 265 if not self.any_tracked:
266 266 return b'?'
267 267 return self.v1_state()
268 268
269 269 @property
270 270 def tracked(self):
271 271 """True is the file is tracked in the working copy"""
272 272 return self._wc_tracked
273 273
274 274 @property
275 275 def any_tracked(self):
276 276 """True is the file is tracked anywhere (wc or parents)"""
277 277 return self._wc_tracked or self._p1_tracked or self._p2_info
278 278
279 279 @property
280 280 def added(self):
281 281 """True if the file has been added"""
282 282 return self._wc_tracked and not (self._p1_tracked or self._p2_info)
283 283
284 284 @property
285 285 def maybe_clean(self):
286 286 """True if the file has a chance to be in the "clean" state"""
287 287 if not self._wc_tracked:
288 288 return False
289 289 elif not self._p1_tracked:
290 290 return False
291 291 elif self._p2_info:
292 292 return False
293 293 return True
294 294
295 295 @property
296 296 def p1_tracked(self):
297 297 """True if the file is tracked in the first parent manifest"""
298 298 return self._p1_tracked
299 299
300 300 @property
301 301 def p2_info(self):
302 302 """True if the file needed to merge or apply any input from p2
303 303
304 304 See the class documentation for details.
305 305 """
306 306 return self._wc_tracked and self._p2_info
307 307
308 308 @property
309 309 def removed(self):
310 310 """True if the file has been removed"""
311 311 return not self._wc_tracked and (self._p1_tracked or self._p2_info)
312 312
313 313 def v1_state(self):
314 314 """return a "state" suitable for v1 serialization"""
315 315 if not self.any_tracked:
316 316 # the object has no state to record, this is -currently-
317 317 # unsupported
318 318 raise RuntimeError('untracked item')
319 319 elif self.removed:
320 320 return b'r'
321 321 elif self._p1_tracked and self._p2_info:
322 322 return b'm'
323 323 elif self.added:
324 324 return b'a'
325 325 else:
326 326 return b'n'
327 327
328 328 def v1_mode(self):
329 329 """return a "mode" suitable for v1 serialization"""
330 330 return self._mode if self._mode is not None else 0
331 331
332 332 def v1_size(self):
333 333 """return a "size" suitable for v1 serialization"""
334 334 if not self.any_tracked:
335 335 # the object has no state to record, this is -currently-
336 336 # unsupported
337 337 raise RuntimeError('untracked item')
338 338 elif self.removed and self._p1_tracked and self._p2_info:
339 339 return NONNORMAL
340 340 elif self._p2_info:
341 341 return FROM_P2
342 342 elif self.removed:
343 343 return 0
344 344 elif self.added:
345 345 return NONNORMAL
346 346 elif self._size is None:
347 347 return NONNORMAL
348 348 else:
349 349 return self._size
350 350
351 351 def v1_mtime(self):
352 352 """return a "mtime" suitable for v1 serialization"""
353 353 if not self.any_tracked:
354 354 # the object has no state to record, this is -currently-
355 355 # unsupported
356 356 raise RuntimeError('untracked item')
357 357 elif self.removed:
358 358 return 0
359 359 elif self._mtime is None:
360 360 return AMBIGUOUS_TIME
361 361 elif self._p2_info:
362 362 return AMBIGUOUS_TIME
363 363 elif not self._p1_tracked:
364 364 return AMBIGUOUS_TIME
365 365 else:
366 366 return self._mtime
367 367
368 368 def need_delay(self, now):
369 369 """True if the stored mtime would be ambiguous with the current time"""
370 370 return self.v1_state() == b'n' and self.v1_mtime() == now
371 371
372 372
373 373 def gettype(q):
374 374 return int(q & 0xFFFF)
375 375
376 376
377 377 class BaseIndexObject(object):
378 378 # Can I be passed to an algorithme implemented in Rust ?
379 379 rust_ext_compat = 0
380 380 # Format of an index entry according to Python's `struct` language
381 381 index_format = revlog_constants.INDEX_ENTRY_V1
382 382 # Size of a C unsigned long long int, platform independent
383 383 big_int_size = struct.calcsize(b'>Q')
384 384 # Size of a C long int, platform independent
385 385 int_size = struct.calcsize(b'>i')
386 386 # An empty index entry, used as a default value to be overridden, or nullrev
387 387 null_item = (
388 388 0,
389 389 0,
390 390 0,
391 391 -1,
392 392 -1,
393 393 -1,
394 394 -1,
395 395 sha1nodeconstants.nullid,
396 396 0,
397 397 0,
398 398 revlog_constants.COMP_MODE_INLINE,
399 399 revlog_constants.COMP_MODE_INLINE,
400 400 )
401 401
402 402 @util.propertycache
403 403 def entry_size(self):
404 404 return self.index_format.size
405 405
406 406 @property
407 407 def nodemap(self):
408 408 msg = b"index.nodemap is deprecated, use index.[has_node|rev|get_rev]"
409 409 util.nouideprecwarn(msg, b'5.3', stacklevel=2)
410 410 return self._nodemap
411 411
412 412 @util.propertycache
413 413 def _nodemap(self):
414 414 nodemap = nodemaputil.NodeMap({sha1nodeconstants.nullid: nullrev})
415 415 for r in range(0, len(self)):
416 416 n = self[r][7]
417 417 nodemap[n] = r
418 418 return nodemap
419 419
420 420 def has_node(self, node):
421 421 """return True if the node exist in the index"""
422 422 return node in self._nodemap
423 423
424 424 def rev(self, node):
425 425 """return a revision for a node
426 426
427 427 If the node is unknown, raise a RevlogError"""
428 428 return self._nodemap[node]
429 429
430 430 def get_rev(self, node):
431 431 """return a revision for a node
432 432
433 433 If the node is unknown, return None"""
434 434 return self._nodemap.get(node)
435 435
436 436 def _stripnodes(self, start):
437 437 if '_nodemap' in vars(self):
438 438 for r in range(start, len(self)):
439 439 n = self[r][7]
440 440 del self._nodemap[n]
441 441
442 442 def clearcaches(self):
443 443 self.__dict__.pop('_nodemap', None)
444 444
445 445 def __len__(self):
446 446 return self._lgt + len(self._extra)
447 447
448 448 def append(self, tup):
449 449 if '_nodemap' in vars(self):
450 450 self._nodemap[tup[7]] = len(self)
451 451 data = self._pack_entry(len(self), tup)
452 452 self._extra.append(data)
453 453
454 454 def _pack_entry(self, rev, entry):
455 455 assert entry[8] == 0
456 456 assert entry[9] == 0
457 457 return self.index_format.pack(*entry[:8])
458 458
459 459 def _check_index(self, i):
460 460 if not isinstance(i, int):
461 461 raise TypeError(b"expecting int indexes")
462 462 if i < 0 or i >= len(self):
463 463 raise IndexError
464 464
465 465 def __getitem__(self, i):
466 466 if i == -1:
467 467 return self.null_item
468 468 self._check_index(i)
469 469 if i >= self._lgt:
470 470 data = self._extra[i - self._lgt]
471 471 else:
472 472 index = self._calculate_index(i)
473 473 data = self._data[index : index + self.entry_size]
474 474 r = self._unpack_entry(i, data)
475 475 if self._lgt and i == 0:
476 476 offset = revlogutils.offset_type(0, gettype(r[0]))
477 477 r = (offset,) + r[1:]
478 478 return r
479 479
480 480 def _unpack_entry(self, rev, data):
481 481 r = self.index_format.unpack(data)
482 482 r = r + (
483 483 0,
484 484 0,
485 485 revlog_constants.COMP_MODE_INLINE,
486 486 revlog_constants.COMP_MODE_INLINE,
487 487 )
488 488 return r
489 489
490 490 def pack_header(self, header):
491 491 """pack header information as binary"""
492 492 v_fmt = revlog_constants.INDEX_HEADER
493 493 return v_fmt.pack(header)
494 494
495 495 def entry_binary(self, rev):
496 496 """return the raw binary string representing a revision"""
497 497 entry = self[rev]
498 498 p = revlog_constants.INDEX_ENTRY_V1.pack(*entry[:8])
499 499 if rev == 0:
500 500 p = p[revlog_constants.INDEX_HEADER.size :]
501 501 return p
502 502
503 503
504 504 class IndexObject(BaseIndexObject):
505 505 def __init__(self, data):
506 506 assert len(data) % self.entry_size == 0, (
507 507 len(data),
508 508 self.entry_size,
509 509 len(data) % self.entry_size,
510 510 )
511 511 self._data = data
512 512 self._lgt = len(data) // self.entry_size
513 513 self._extra = []
514 514
515 515 def _calculate_index(self, i):
516 516 return i * self.entry_size
517 517
518 518 def __delitem__(self, i):
519 519 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
520 520 raise ValueError(b"deleting slices only supports a:-1 with step 1")
521 521 i = i.start
522 522 self._check_index(i)
523 523 self._stripnodes(i)
524 524 if i < self._lgt:
525 525 self._data = self._data[: i * self.entry_size]
526 526 self._lgt = i
527 527 self._extra = []
528 528 else:
529 529 self._extra = self._extra[: i - self._lgt]
530 530
531 531
532 532 class PersistentNodeMapIndexObject(IndexObject):
533 533 """a Debug oriented class to test persistent nodemap
534 534
535 535 We need a simple python object to test API and higher level behavior. See
536 536 the Rust implementation for more serious usage. This should be used only
537 537 through the dedicated `devel.persistent-nodemap` config.
538 538 """
539 539
540 540 def nodemap_data_all(self):
541 541 """Return bytes containing a full serialization of a nodemap
542 542
543 543 The nodemap should be valid for the full set of revisions in the
544 544 index."""
545 545 return nodemaputil.persistent_data(self)
546 546
547 547 def nodemap_data_incremental(self):
548 548 """Return bytes containing a incremental update to persistent nodemap
549 549
550 550 This containst the data for an append-only update of the data provided
551 551 in the last call to `update_nodemap_data`.
552 552 """
553 553 if self._nm_root is None:
554 554 return None
555 555 docket = self._nm_docket
556 556 changed, data = nodemaputil.update_persistent_data(
557 557 self, self._nm_root, self._nm_max_idx, self._nm_docket.tip_rev
558 558 )
559 559
560 560 self._nm_root = self._nm_max_idx = self._nm_docket = None
561 561 return docket, changed, data
562 562
563 563 def update_nodemap_data(self, docket, nm_data):
564 564 """provide full block of persisted binary data for a nodemap
565 565
566 566 The data are expected to come from disk. See `nodemap_data_all` for a
567 567 produceur of such data."""
568 568 if nm_data is not None:
569 569 self._nm_root, self._nm_max_idx = nodemaputil.parse_data(nm_data)
570 570 if self._nm_root:
571 571 self._nm_docket = docket
572 572 else:
573 573 self._nm_root = self._nm_max_idx = self._nm_docket = None
574 574
575 575
576 576 class InlinedIndexObject(BaseIndexObject):
577 577 def __init__(self, data, inline=0):
578 578 self._data = data
579 579 self._lgt = self._inline_scan(None)
580 580 self._inline_scan(self._lgt)
581 581 self._extra = []
582 582
583 583 def _inline_scan(self, lgt):
584 584 off = 0
585 585 if lgt is not None:
586 586 self._offsets = [0] * lgt
587 587 count = 0
588 588 while off <= len(self._data) - self.entry_size:
589 589 start = off + self.big_int_size
590 590 (s,) = struct.unpack(
591 591 b'>i',
592 592 self._data[start : start + self.int_size],
593 593 )
594 594 if lgt is not None:
595 595 self._offsets[count] = off
596 596 count += 1
597 597 off += self.entry_size + s
598 598 if off != len(self._data):
599 599 raise ValueError(b"corrupted data")
600 600 return count
601 601
602 602 def __delitem__(self, i):
603 603 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
604 604 raise ValueError(b"deleting slices only supports a:-1 with step 1")
605 605 i = i.start
606 606 self._check_index(i)
607 607 self._stripnodes(i)
608 608 if i < self._lgt:
609 609 self._offsets = self._offsets[:i]
610 610 self._lgt = i
611 611 self._extra = []
612 612 else:
613 613 self._extra = self._extra[: i - self._lgt]
614 614
615 615 def _calculate_index(self, i):
616 616 return self._offsets[i]
617 617
618 618
619 619 def parse_index2(data, inline, revlogv2=False):
620 620 if not inline:
621 621 cls = IndexObject2 if revlogv2 else IndexObject
622 622 return cls(data), None
623 623 cls = InlinedIndexObject
624 624 return cls(data, inline), (0, data)
625 625
626 626
627 627 def parse_index_cl_v2(data):
628 628 return IndexChangelogV2(data), None
629 629
630 630
631 631 class IndexObject2(IndexObject):
632 632 index_format = revlog_constants.INDEX_ENTRY_V2
633 633
634 634 def replace_sidedata_info(
635 635 self,
636 636 rev,
637 637 sidedata_offset,
638 638 sidedata_length,
639 639 offset_flags,
640 640 compression_mode,
641 641 ):
642 642 """
643 643 Replace an existing index entry's sidedata offset and length with new
644 644 ones.
645 645 This cannot be used outside of the context of sidedata rewriting,
646 646 inside the transaction that creates the revision `rev`.
647 647 """
648 648 if rev < 0:
649 649 raise KeyError
650 650 self._check_index(rev)
651 651 if rev < self._lgt:
652 652 msg = b"cannot rewrite entries outside of this transaction"
653 653 raise KeyError(msg)
654 654 else:
655 655 entry = list(self[rev])
656 656 entry[0] = offset_flags
657 657 entry[8] = sidedata_offset
658 658 entry[9] = sidedata_length
659 659 entry[11] = compression_mode
660 660 entry = tuple(entry)
661 661 new = self._pack_entry(rev, entry)
662 662 self._extra[rev - self._lgt] = new
663 663
664 664 def _unpack_entry(self, rev, data):
665 665 data = self.index_format.unpack(data)
666 666 entry = data[:10]
667 667 data_comp = data[10] & 3
668 668 sidedata_comp = (data[10] & (3 << 2)) >> 2
669 669 return entry + (data_comp, sidedata_comp)
670 670
671 671 def _pack_entry(self, rev, entry):
672 672 data = entry[:10]
673 673 data_comp = entry[10] & 3
674 674 sidedata_comp = (entry[11] & 3) << 2
675 675 data += (data_comp | sidedata_comp,)
676 676
677 677 return self.index_format.pack(*data)
678 678
679 679 def entry_binary(self, rev):
680 680 """return the raw binary string representing a revision"""
681 681 entry = self[rev]
682 682 return self._pack_entry(rev, entry)
683 683
684 684 def pack_header(self, header):
685 685 """pack header information as binary"""
686 686 msg = 'version header should go in the docket, not the index: %d'
687 687 msg %= header
688 688 raise error.ProgrammingError(msg)
689 689
690 690
691 691 class IndexChangelogV2(IndexObject2):
692 692 index_format = revlog_constants.INDEX_ENTRY_CL_V2
693 693
694 694 def _unpack_entry(self, rev, data, r=True):
695 695 items = self.index_format.unpack(data)
696 696 entry = items[:3] + (rev, rev) + items[3:8]
697 697 data_comp = items[8] & 3
698 698 sidedata_comp = (items[8] >> 2) & 3
699 699 return entry + (data_comp, sidedata_comp)
700 700
701 701 def _pack_entry(self, rev, entry):
702 702 assert entry[3] == rev, entry[3]
703 703 assert entry[4] == rev, entry[4]
704 704 data = entry[:3] + entry[5:10]
705 705 data_comp = entry[10] & 3
706 706 sidedata_comp = (entry[11] & 3) << 2
707 707 data += (data_comp | sidedata_comp,)
708 708 return self.index_format.pack(*data)
709 709
710 710
711 711 def parse_index_devel_nodemap(data, inline):
712 712 """like parse_index2, but alway return a PersistentNodeMapIndexObject"""
713 713 return PersistentNodeMapIndexObject(data), None
714 714
715 715
716 716 def parse_dirstate(dmap, copymap, st):
717 717 parents = [st[:20], st[20:40]]
718 718 # dereference fields so they will be local in loop
719 719 format = b">cllll"
720 720 e_size = struct.calcsize(format)
721 721 pos1 = 40
722 722 l = len(st)
723 723
724 724 # the inner loop
725 725 while pos1 < l:
726 726 pos2 = pos1 + e_size
727 727 e = _unpack(b">cllll", st[pos1:pos2]) # a literal here is faster
728 728 pos1 = pos2 + e[4]
729 729 f = st[pos2:pos1]
730 730 if b'\0' in f:
731 731 f, c = f.split(b'\0')
732 732 copymap[f] = c
733 733 dmap[f] = DirstateItem.from_v1_data(*e[:4])
734 734 return parents
735 735
736 736
737 737 def pack_dirstate(dmap, copymap, pl, now):
738 738 now = int(now)
739 739 cs = stringio()
740 740 write = cs.write
741 741 write(b"".join(pl))
742 742 for f, e in pycompat.iteritems(dmap):
743 743 if e.need_delay(now):
744 744 # The file was last modified "simultaneously" with the current
745 745 # write to dirstate (i.e. within the same second for file-
746 746 # systems with a granularity of 1 sec). This commonly happens
747 747 # for at least a couple of files on 'update'.
748 748 # The user could change the file without changing its size
749 749 # within the same second. Invalidate the file's mtime in
750 750 # dirstate, forcing future 'status' calls to compare the
751 751 # contents of the file if the size is the same. This prevents
752 752 # mistakenly treating such files as clean.
753 753 e.set_possibly_dirty()
754 754
755 755 if f in copymap:
756 756 f = b"%s\0%s" % (f, copymap[f])
757 757 e = _pack(
758 758 b">cllll",
759 759 e.v1_state(),
760 760 e.v1_mode(),
761 761 e.v1_size(),
762 762 e.v1_mtime(),
763 763 len(f),
764 764 )
765 765 write(e)
766 766 write(f)
767 767 return cs.getvalue()
@@ -1,422 +1,426 b''
1 1 use crate::errors::HgError;
2 2 use bitflags::bitflags;
3 3 use std::convert::TryFrom;
4 4
5 5 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
6 6 pub enum EntryState {
7 7 Normal,
8 8 Added,
9 9 Removed,
10 10 Merged,
11 11 }
12 12
13 13 /// The C implementation uses all signed types. This will be an issue
14 14 /// either when 4GB+ source files are commonplace or in 2038, whichever
15 15 /// comes first.
16 16 #[derive(Debug, PartialEq, Copy, Clone)]
17 17 pub struct DirstateEntry {
18 18 pub(crate) flags: Flags,
19 19 mode_size: Option<(i32, i32)>,
20 20 mtime: Option<i32>,
21 21 }
22 22
23 23 bitflags! {
24 24 pub(crate) struct Flags: u8 {
25 25 const WDIR_TRACKED = 1 << 0;
26 26 const P1_TRACKED = 1 << 1;
27 27 const P2_INFO = 1 << 2;
28 28 }
29 29 }
30 30
31 31 pub const V1_RANGEMASK: i32 = 0x7FFFFFFF;
32 32
33 33 pub const MTIME_UNSET: i32 = -1;
34 34
35 35 /// A `DirstateEntry` with a size of `-2` means that it was merged from the
36 36 /// other parent. This allows revert to pick the right status back during a
37 37 /// merge.
38 38 pub const SIZE_FROM_OTHER_PARENT: i32 = -2;
39 39 /// A special value used for internal representation of special case in
40 40 /// dirstate v1 format.
41 41 pub const SIZE_NON_NORMAL: i32 = -1;
42 42
43 43 impl DirstateEntry {
44 44 pub fn from_v2_data(
45 45 wdir_tracked: bool,
46 46 p1_tracked: bool,
47 47 p2_info: bool,
48 48 mode_size: Option<(i32, i32)>,
49 49 mtime: Option<i32>,
50 50 ) -> Self {
51 51 let mut flags = Flags::empty();
52 52 flags.set(Flags::WDIR_TRACKED, wdir_tracked);
53 53 flags.set(Flags::P1_TRACKED, p1_tracked);
54 54 flags.set(Flags::P2_INFO, p2_info);
55 55 Self {
56 56 flags,
57 57 mode_size,
58 58 mtime,
59 59 }
60 60 }
61 61
62 62 pub fn from_v1_data(
63 63 state: EntryState,
64 64 mode: i32,
65 65 size: i32,
66 66 mtime: i32,
67 67 ) -> Self {
68 68 match state {
69 69 EntryState::Normal => {
70 70 if size == SIZE_FROM_OTHER_PARENT {
71 71 Self::new_from_p2()
72 72 } else if size == SIZE_NON_NORMAL {
73 73 Self::new_possibly_dirty()
74 74 } else if mtime == MTIME_UNSET {
75 75 Self {
76 76 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
77 77 mode_size: Some((mode, size)),
78 78 mtime: None,
79 79 }
80 80 } else {
81 81 Self::new_normal(mode, size, mtime)
82 82 }
83 83 }
84 EntryState::Added => Self::new_added(),
84 EntryState::Added => Self {
85 flags: Flags::WDIR_TRACKED,
86 mode_size: None,
87 mtime: None,
88 },
85 89 EntryState::Removed => Self {
86 90 flags: if size == SIZE_NON_NORMAL {
87 91 Flags::P1_TRACKED | Flags::P2_INFO
88 92 } else if size == SIZE_FROM_OTHER_PARENT {
89 93 // We don’t know if P1_TRACKED should be set (file history)
90 94 Flags::P2_INFO
91 95 } else {
92 96 Flags::P1_TRACKED
93 97 },
94 98 mode_size: None,
95 99 mtime: None,
96 100 },
97 101 EntryState::Merged => Self {
98 102 flags: Flags::WDIR_TRACKED
99 103 | Flags::P1_TRACKED // might not be true because of rename ?
100 104 | Flags::P2_INFO, // might not be true because of rename ?
101 105 mode_size: None,
102 106 mtime: None,
103 107 },
104 108 }
105 109 }
106 110
107 111 pub fn new_from_p2() -> Self {
108 112 Self {
109 113 // might be missing P1_TRACKED
110 114 flags: Flags::WDIR_TRACKED | Flags::P2_INFO,
111 115 mode_size: None,
112 116 mtime: None,
113 117 }
114 118 }
115 119
116 120 pub fn new_possibly_dirty() -> Self {
117 121 Self {
118 122 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
119 123 mode_size: None,
120 124 mtime: None,
121 125 }
122 126 }
123 127
124 128 pub fn new_added() -> Self {
125 129 Self {
126 130 flags: Flags::WDIR_TRACKED,
127 131 mode_size: None,
128 132 mtime: None,
129 133 }
130 134 }
131 135
132 136 pub fn new_normal(mode: i32, size: i32, mtime: i32) -> Self {
133 137 Self {
134 138 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
135 139 mode_size: Some((mode, size)),
136 140 mtime: Some(mtime),
137 141 }
138 142 }
139 143
140 144 /// Creates a new entry in "removed" state.
141 145 ///
142 146 /// `size` is expected to be zero, `SIZE_NON_NORMAL`, or
143 147 /// `SIZE_FROM_OTHER_PARENT`
144 148 pub fn new_removed(size: i32) -> Self {
145 149 Self::from_v1_data(EntryState::Removed, 0, size, 0)
146 150 }
147 151
148 152 pub fn tracked(&self) -> bool {
149 153 self.flags.contains(Flags::WDIR_TRACKED)
150 154 }
151 155
152 156 pub fn p1_tracked(&self) -> bool {
153 157 self.flags.contains(Flags::P1_TRACKED)
154 158 }
155 159
156 160 fn in_either_parent(&self) -> bool {
157 161 self.flags.intersects(Flags::P1_TRACKED | Flags::P2_INFO)
158 162 }
159 163
160 164 pub fn removed(&self) -> bool {
161 165 self.in_either_parent() && !self.flags.contains(Flags::WDIR_TRACKED)
162 166 }
163 167
164 168 pub fn p2_info(&self) -> bool {
165 169 self.flags.contains(Flags::WDIR_TRACKED | Flags::P2_INFO)
166 170 }
167 171
168 172 pub fn added(&self) -> bool {
169 173 self.flags.contains(Flags::WDIR_TRACKED) && !self.in_either_parent()
170 174 }
171 175
172 176 pub fn maybe_clean(&self) -> bool {
173 177 if !self.flags.contains(Flags::WDIR_TRACKED) {
174 178 false
175 179 } else if !self.flags.contains(Flags::P1_TRACKED) {
176 180 false
177 181 } else if self.flags.contains(Flags::P2_INFO) {
178 182 false
179 183 } else {
180 184 true
181 185 }
182 186 }
183 187
184 188 pub fn any_tracked(&self) -> bool {
185 189 self.flags.intersects(
186 190 Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO,
187 191 )
188 192 }
189 193
190 194 /// Returns `(wdir_tracked, p1_tracked, p2_info, mode_size, mtime)`
191 195 pub(crate) fn v2_data(
192 196 &self,
193 197 ) -> (bool, bool, bool, Option<(i32, i32)>, Option<i32>) {
194 198 if !self.any_tracked() {
195 199 // TODO: return an Option instead?
196 200 panic!("Accessing v1_state of an untracked DirstateEntry")
197 201 }
198 202 let wdir_tracked = self.flags.contains(Flags::WDIR_TRACKED);
199 203 let p1_tracked = self.flags.contains(Flags::P1_TRACKED);
200 204 let p2_info = self.flags.contains(Flags::P2_INFO);
201 205 let mode_size = self.mode_size;
202 206 let mtime = self.mtime;
203 207 (wdir_tracked, p1_tracked, p2_info, mode_size, mtime)
204 208 }
205 209
206 210 fn v1_state(&self) -> EntryState {
207 211 if !self.any_tracked() {
208 212 // TODO: return an Option instead?
209 213 panic!("Accessing v1_state of an untracked DirstateEntry")
210 214 }
211 215 if self.removed() {
212 216 EntryState::Removed
213 217 } else if self
214 218 .flags
215 219 .contains(Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_INFO)
216 220 {
217 221 EntryState::Merged
218 222 } else if self.added() {
219 223 EntryState::Added
220 224 } else {
221 225 EntryState::Normal
222 226 }
223 227 }
224 228
225 229 fn v1_mode(&self) -> i32 {
226 230 if let Some((mode, _size)) = self.mode_size {
227 231 mode
228 232 } else {
229 233 0
230 234 }
231 235 }
232 236
233 237 fn v1_size(&self) -> i32 {
234 238 if !self.any_tracked() {
235 239 // TODO: return an Option instead?
236 240 panic!("Accessing v1_size of an untracked DirstateEntry")
237 241 }
238 242 if self.removed()
239 243 && self.flags.contains(Flags::P1_TRACKED | Flags::P2_INFO)
240 244 {
241 245 SIZE_NON_NORMAL
242 246 } else if self.flags.contains(Flags::P2_INFO) {
243 247 SIZE_FROM_OTHER_PARENT
244 248 } else if self.removed() {
245 249 0
246 250 } else if self.added() {
247 251 SIZE_NON_NORMAL
248 252 } else if let Some((_mode, size)) = self.mode_size {
249 253 size
250 254 } else {
251 255 SIZE_NON_NORMAL
252 256 }
253 257 }
254 258
255 259 fn v1_mtime(&self) -> i32 {
256 260 if !self.any_tracked() {
257 261 // TODO: return an Option instead?
258 262 panic!("Accessing v1_mtime of an untracked DirstateEntry")
259 263 }
260 264 if self.removed() {
261 265 0
262 266 } else if self.flags.contains(Flags::P2_INFO) {
263 267 MTIME_UNSET
264 268 } else if !self.flags.contains(Flags::P1_TRACKED) {
265 269 MTIME_UNSET
266 270 } else {
267 271 self.mtime.unwrap_or(MTIME_UNSET)
268 272 }
269 273 }
270 274
271 275 // TODO: return `Option<EntryState>`? None when `!self.any_tracked`
272 276 pub fn state(&self) -> EntryState {
273 277 self.v1_state()
274 278 }
275 279
276 280 // TODO: return Option?
277 281 pub fn mode(&self) -> i32 {
278 282 self.v1_mode()
279 283 }
280 284
281 285 // TODO: return Option?
282 286 pub fn size(&self) -> i32 {
283 287 self.v1_size()
284 288 }
285 289
286 290 // TODO: return Option?
287 291 pub fn mtime(&self) -> i32 {
288 292 self.v1_mtime()
289 293 }
290 294
291 295 pub fn drop_merge_data(&mut self) {
292 296 if self.flags.contains(Flags::P2_INFO) {
293 297 self.flags.remove(Flags::P2_INFO);
294 298 self.mode_size = None;
295 299 self.mtime = None;
296 300 }
297 301 }
298 302
299 303 pub fn set_possibly_dirty(&mut self) {
300 304 self.mtime = None
301 305 }
302 306
303 307 pub fn set_clean(&mut self, mode: i32, size: i32, mtime: i32) {
304 308 self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED);
305 309 self.mode_size = Some((mode, size));
306 310 self.mtime = Some(mtime);
307 311 }
308 312
309 313 pub fn set_tracked(&mut self) {
310 314 self.flags.insert(Flags::WDIR_TRACKED);
311 315 // `set_tracked` is replacing various `normallookup` call. So we mark
312 316 // the files as needing lookup
313 317 //
314 318 // Consider dropping this in the future in favor of something less
315 319 // broad.
316 320 self.mtime = None;
317 321 }
318 322
319 323 pub fn set_untracked(&mut self) {
320 324 self.flags.remove(Flags::WDIR_TRACKED);
321 325 self.mode_size = None;
322 326 self.mtime = None;
323 327 }
324 328
325 329 /// Returns `(state, mode, size, mtime)` for the puprose of serialization
326 330 /// in the dirstate-v1 format.
327 331 ///
328 332 /// This includes marker values such as `mtime == -1`. In the future we may
329 333 /// want to not represent these cases that way in memory, but serialization
330 334 /// will need to keep the same format.
331 335 pub fn v1_data(&self) -> (u8, i32, i32, i32) {
332 336 (
333 337 self.v1_state().into(),
334 338 self.v1_mode(),
335 339 self.v1_size(),
336 340 self.v1_mtime(),
337 341 )
338 342 }
339 343
340 344 pub(crate) fn is_from_other_parent(&self) -> bool {
341 345 self.state() == EntryState::Normal
342 346 && self.size() == SIZE_FROM_OTHER_PARENT
343 347 }
344 348
345 349 // TODO: other platforms
346 350 #[cfg(unix)]
347 351 pub fn mode_changed(
348 352 &self,
349 353 filesystem_metadata: &std::fs::Metadata,
350 354 ) -> bool {
351 355 use std::os::unix::fs::MetadataExt;
352 356 const EXEC_BIT_MASK: u32 = 0o100;
353 357 let dirstate_exec_bit = (self.mode() as u32) & EXEC_BIT_MASK;
354 358 let fs_exec_bit = filesystem_metadata.mode() & EXEC_BIT_MASK;
355 359 dirstate_exec_bit != fs_exec_bit
356 360 }
357 361
358 362 /// Returns a `(state, mode, size, mtime)` tuple as for
359 363 /// `DirstateMapMethods::debug_iter`.
360 364 pub fn debug_tuple(&self) -> (u8, i32, i32, i32) {
361 365 (self.state().into(), self.mode(), self.size(), self.mtime())
362 366 }
363 367
364 368 pub fn mtime_is_ambiguous(&self, now: i32) -> bool {
365 369 self.state() == EntryState::Normal && self.mtime() == now
366 370 }
367 371
368 372 pub fn clear_ambiguous_mtime(&mut self, now: i32) -> bool {
369 373 let ambiguous = self.mtime_is_ambiguous(now);
370 374 if ambiguous {
371 375 // The file was last modified "simultaneously" with the current
372 376 // write to dirstate (i.e. within the same second for file-
373 377 // systems with a granularity of 1 sec). This commonly happens
374 378 // for at least a couple of files on 'update'.
375 379 // The user could change the file without changing its size
376 380 // within the same second. Invalidate the file's mtime in
377 381 // dirstate, forcing future 'status' calls to compare the
378 382 // contents of the file if the size is the same. This prevents
379 383 // mistakenly treating such files as clean.
380 384 self.set_possibly_dirty()
381 385 }
382 386 ambiguous
383 387 }
384 388 }
385 389
386 390 impl EntryState {
387 391 pub fn is_tracked(self) -> bool {
388 392 use EntryState::*;
389 393 match self {
390 394 Normal | Added | Merged => true,
391 395 Removed => false,
392 396 }
393 397 }
394 398 }
395 399
396 400 impl TryFrom<u8> for EntryState {
397 401 type Error = HgError;
398 402
399 403 fn try_from(value: u8) -> Result<Self, Self::Error> {
400 404 match value {
401 405 b'n' => Ok(EntryState::Normal),
402 406 b'a' => Ok(EntryState::Added),
403 407 b'r' => Ok(EntryState::Removed),
404 408 b'm' => Ok(EntryState::Merged),
405 409 _ => Err(HgError::CorruptedRepository(format!(
406 410 "Incorrect dirstate entry state {}",
407 411 value
408 412 ))),
409 413 }
410 414 }
411 415 }
412 416
413 417 impl Into<u8> for EntryState {
414 418 fn into(self) -> u8 {
415 419 match self {
416 420 EntryState::Normal => b'n',
417 421 EntryState::Added => b'a',
418 422 EntryState::Removed => b'r',
419 423 EntryState::Merged => b'm',
420 424 }
421 425 }
422 426 }
General Comments 0
You need to be logged in to leave comments. Login now