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