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