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