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