##// END OF EJS Templates
obsolete: simplify relevantmarker...
Joerg Sonnenberger -
r52538:ff523675 default
parent child Browse files
Show More
@@ -1,1170 +1,1169 b''
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete marker handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewrite operations, and help
18 18 building new tools to reconcile conflicting rewrite actions. To
19 19 facilitate conflict resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called a "predecessor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a predecessor are called "successor markers of X" because they hold
26 26 information about the successors of X. Markers that use changeset Y as
27 27 a successors are call "predecessor markers of Y" because they hold
28 28 information about the predecessors of Y.
29 29
30 30 Examples:
31 31
32 32 - When changeset A is replaced by changeset A', one marker is stored:
33 33
34 34 (A, (A',))
35 35
36 36 - When changesets A and B are folded into a new changeset C, two markers are
37 37 stored:
38 38
39 39 (A, (C,)) and (B, (C,))
40 40
41 41 - When changeset A is simply "pruned" from the graph, a marker is created:
42 42
43 43 (A, ())
44 44
45 45 - When changeset A is split into B and C, a single marker is used:
46 46
47 47 (A, (B, C))
48 48
49 49 We use a single marker to distinguish the "split" case from the "divergence"
50 50 case. If two independent operations rewrite the same changeset A in to A' and
51 51 A'', we have an error case: divergent rewriting. We can detect it because
52 52 two markers will be created independently:
53 53
54 54 (A, (B,)) and (A, (C,))
55 55
56 56 Format
57 57 ------
58 58
59 59 Markers are stored in an append-only file stored in
60 60 '.hg/store/obsstore'.
61 61
62 62 The file starts with a version header:
63 63
64 64 - 1 unsigned byte: version number, starting at zero.
65 65
66 66 The header is followed by the markers. Marker format depend of the version. See
67 67 comment associated with each format for details.
68 68
69 69 """
70 70
71 71 import binascii
72 72 import struct
73 73 import weakref
74 74
75 75 from .i18n import _
76 76 from .node import (
77 77 bin,
78 78 hex,
79 79 )
80 80 from . import (
81 81 encoding,
82 82 error,
83 83 obsutil,
84 84 phases,
85 85 policy,
86 86 pycompat,
87 87 util,
88 88 )
89 89 from .utils import (
90 90 dateutil,
91 91 hashutil,
92 92 )
93 93
94 94 parsers = policy.importmod('parsers')
95 95
96 96 _pack = struct.pack
97 97 _unpack = struct.unpack
98 98 _calcsize = struct.calcsize
99 99 propertycache = util.propertycache
100 100
101 101 # Options for obsolescence
102 102 createmarkersopt = b'createmarkers'
103 103 allowunstableopt = b'allowunstable'
104 104 allowdivergenceopt = b'allowdivergence'
105 105 exchangeopt = b'exchange'
106 106
107 107
108 108 def _getoptionvalue(repo, option):
109 109 """Returns True if the given repository has the given obsolete option
110 110 enabled.
111 111 """
112 112 configkey = b'evolution.%s' % option
113 113 newconfig = repo.ui.configbool(b'experimental', configkey)
114 114
115 115 # Return the value only if defined
116 116 if newconfig is not None:
117 117 return newconfig
118 118
119 119 # Fallback on generic option
120 120 try:
121 121 return repo.ui.configbool(b'experimental', b'evolution')
122 122 except (error.ConfigError, AttributeError):
123 123 # Fallback on old-fashion config
124 124 # inconsistent config: experimental.evolution
125 125 result = set(repo.ui.configlist(b'experimental', b'evolution'))
126 126
127 127 if b'all' in result:
128 128 return True
129 129
130 130 # Temporary hack for next check
131 131 newconfig = repo.ui.config(b'experimental', b'evolution.createmarkers')
132 132 if newconfig:
133 133 result.add(b'createmarkers')
134 134
135 135 return option in result
136 136
137 137
138 138 def getoptions(repo):
139 139 """Returns dicts showing state of obsolescence features."""
140 140
141 141 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
142 142 if createmarkersvalue:
143 143 unstablevalue = _getoptionvalue(repo, allowunstableopt)
144 144 divergencevalue = _getoptionvalue(repo, allowdivergenceopt)
145 145 exchangevalue = _getoptionvalue(repo, exchangeopt)
146 146 else:
147 147 # if we cannot create obsolescence markers, we shouldn't exchange them
148 148 # or perform operations that lead to instability or divergence
149 149 unstablevalue = False
150 150 divergencevalue = False
151 151 exchangevalue = False
152 152
153 153 return {
154 154 createmarkersopt: createmarkersvalue,
155 155 allowunstableopt: unstablevalue,
156 156 allowdivergenceopt: divergencevalue,
157 157 exchangeopt: exchangevalue,
158 158 }
159 159
160 160
161 161 def isenabled(repo, option):
162 162 """Returns True if the given repository has the given obsolete option
163 163 enabled.
164 164 """
165 165 return getoptions(repo)[option]
166 166
167 167
168 168 # Creating aliases for marker flags because evolve extension looks for
169 169 # bumpedfix in obsolete.py
170 170 bumpedfix = obsutil.bumpedfix
171 171 usingsha256 = obsutil.usingsha256
172 172
173 173 ## Parsing and writing of version "0"
174 174 #
175 175 # The header is followed by the markers. Each marker is made of:
176 176 #
177 177 # - 1 uint8 : number of new changesets "N", can be zero.
178 178 #
179 179 # - 1 uint32: metadata size "M" in bytes.
180 180 #
181 181 # - 1 byte: a bit field. It is reserved for flags used in common
182 182 # obsolete marker operations, to avoid repeated decoding of metadata
183 183 # entries.
184 184 #
185 185 # - 20 bytes: obsoleted changeset identifier.
186 186 #
187 187 # - N*20 bytes: new changesets identifiers.
188 188 #
189 189 # - M bytes: metadata as a sequence of nul-terminated strings. Each
190 190 # string contains a key and a value, separated by a colon ':', without
191 191 # additional encoding. Keys cannot contain '\0' or ':' and values
192 192 # cannot contain '\0'.
193 193 _fm0version = 0
194 194 _fm0fixed = b'>BIB20s'
195 195 _fm0node = b'20s'
196 196 _fm0fsize = _calcsize(_fm0fixed)
197 197 _fm0fnodesize = _calcsize(_fm0node)
198 198
199 199
200 200 def _fm0readmarkers(data, off, stop):
201 201 # Loop on markers
202 202 while off < stop:
203 203 # read fixed part
204 204 cur = data[off : off + _fm0fsize]
205 205 off += _fm0fsize
206 206 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
207 207 # read replacement
208 208 sucs = ()
209 209 if numsuc:
210 210 s = _fm0fnodesize * numsuc
211 211 cur = data[off : off + s]
212 212 sucs = _unpack(_fm0node * numsuc, cur)
213 213 off += s
214 214 # read metadata
215 215 # (metadata will be decoded on demand)
216 216 metadata = data[off : off + mdsize]
217 217 if len(metadata) != mdsize:
218 218 raise error.Abort(
219 219 _(
220 220 b'parsing obsolete marker: metadata is too '
221 221 b'short, %d bytes expected, got %d'
222 222 )
223 223 % (mdsize, len(metadata))
224 224 )
225 225 off += mdsize
226 226 metadata = _fm0decodemeta(metadata)
227 227 try:
228 228 when, offset = metadata.pop(b'date', b'0 0').split(b' ')
229 229 date = float(when), int(offset)
230 230 except ValueError:
231 231 date = (0.0, 0)
232 232 parents = None
233 233 if b'p2' in metadata:
234 234 parents = (metadata.pop(b'p1', None), metadata.pop(b'p2', None))
235 235 elif b'p1' in metadata:
236 236 parents = (metadata.pop(b'p1', None),)
237 237 elif b'p0' in metadata:
238 238 parents = ()
239 239 if parents is not None:
240 240 try:
241 241 parents = tuple(bin(p) for p in parents)
242 242 # if parent content is not a nodeid, drop the data
243 243 for p in parents:
244 244 if len(p) != 20:
245 245 parents = None
246 246 break
247 247 except binascii.Error:
248 248 # if content cannot be translated to nodeid drop the data.
249 249 parents = None
250 250
251 251 metadata = tuple(sorted(metadata.items()))
252 252
253 253 yield (pre, sucs, flags, metadata, date, parents)
254 254
255 255
256 256 def _fm0encodeonemarker(marker):
257 257 pre, sucs, flags, metadata, date, parents = marker
258 258 if flags & usingsha256:
259 259 raise error.Abort(_(b'cannot handle sha256 with old obsstore format'))
260 260 metadata = dict(metadata)
261 261 time, tz = date
262 262 metadata[b'date'] = b'%r %i' % (time, tz)
263 263 if parents is not None:
264 264 if not parents:
265 265 # mark that we explicitly recorded no parents
266 266 metadata[b'p0'] = b''
267 267 for i, p in enumerate(parents, 1):
268 268 metadata[b'p%i' % i] = hex(p)
269 269 metadata = _fm0encodemeta(metadata)
270 270 numsuc = len(sucs)
271 271 format = _fm0fixed + (_fm0node * numsuc)
272 272 data = [numsuc, len(metadata), flags, pre]
273 273 data.extend(sucs)
274 274 return _pack(format, *data) + metadata
275 275
276 276
277 277 def _fm0encodemeta(meta):
278 278 """Return encoded metadata string to string mapping.
279 279
280 280 Assume no ':' in key and no '\0' in both key and value."""
281 281 for key, value in meta.items():
282 282 if b':' in key or b'\0' in key:
283 283 raise ValueError(b"':' and '\0' are forbidden in metadata key'")
284 284 if b'\0' in value:
285 285 raise ValueError(b"':' is forbidden in metadata value'")
286 286 return b'\0'.join([b'%s:%s' % (k, meta[k]) for k in sorted(meta)])
287 287
288 288
289 289 def _fm0decodemeta(data):
290 290 """Return string to string dictionary from encoded version."""
291 291 d = {}
292 292 for l in data.split(b'\0'):
293 293 if l:
294 294 key, value = l.split(b':', 1)
295 295 d[key] = value
296 296 return d
297 297
298 298
299 299 ## Parsing and writing of version "1"
300 300 #
301 301 # The header is followed by the markers. Each marker is made of:
302 302 #
303 303 # - uint32: total size of the marker (including this field)
304 304 #
305 305 # - float64: date in seconds since epoch
306 306 #
307 307 # - int16: timezone offset in minutes
308 308 #
309 309 # - uint16: a bit field. It is reserved for flags used in common
310 310 # obsolete marker operations, to avoid repeated decoding of metadata
311 311 # entries.
312 312 #
313 313 # - uint8: number of successors "N", can be zero.
314 314 #
315 315 # - uint8: number of parents "P", can be zero.
316 316 #
317 317 # 0: parents data stored but no parent,
318 318 # 1: one parent stored,
319 319 # 2: two parents stored,
320 320 # 3: no parent data stored
321 321 #
322 322 # - uint8: number of metadata entries M
323 323 #
324 324 # - 20 or 32 bytes: predecessor changeset identifier.
325 325 #
326 326 # - N*(20 or 32) bytes: successors changesets identifiers.
327 327 #
328 328 # - P*(20 or 32) bytes: parents of the predecessors changesets.
329 329 #
330 330 # - M*(uint8, uint8): size of all metadata entries (key and value)
331 331 #
332 332 # - remaining bytes: the metadata, each (key, value) pair after the other.
333 333 _fm1version = 1
334 334 _fm1fixed = b'>IdhHBBB'
335 335 _fm1nodesha1 = b'20s'
336 336 _fm1nodesha256 = b'32s'
337 337 _fm1nodesha1size = _calcsize(_fm1nodesha1)
338 338 _fm1nodesha256size = _calcsize(_fm1nodesha256)
339 339 _fm1fsize = _calcsize(_fm1fixed)
340 340 _fm1parentnone = 3
341 341 _fm1metapair = b'BB'
342 342 _fm1metapairsize = _calcsize(_fm1metapair)
343 343
344 344
345 345 def _fm1purereadmarkers(data, off, stop):
346 346 # make some global constants local for performance
347 347 noneflag = _fm1parentnone
348 348 sha2flag = usingsha256
349 349 sha1size = _fm1nodesha1size
350 350 sha2size = _fm1nodesha256size
351 351 sha1fmt = _fm1nodesha1
352 352 sha2fmt = _fm1nodesha256
353 353 metasize = _fm1metapairsize
354 354 metafmt = _fm1metapair
355 355 fsize = _fm1fsize
356 356 unpack = _unpack
357 357
358 358 # Loop on markers
359 359 ufixed = struct.Struct(_fm1fixed).unpack
360 360
361 361 while off < stop:
362 362 # read fixed part
363 363 o1 = off + fsize
364 364 t, secs, tz, flags, numsuc, numpar, nummeta = ufixed(data[off:o1])
365 365
366 366 if flags & sha2flag:
367 367 nodefmt = sha2fmt
368 368 nodesize = sha2size
369 369 else:
370 370 nodefmt = sha1fmt
371 371 nodesize = sha1size
372 372
373 373 (prec,) = unpack(nodefmt, data[o1 : o1 + nodesize])
374 374 o1 += nodesize
375 375
376 376 # read 0 or more successors
377 377 if numsuc == 1:
378 378 o2 = o1 + nodesize
379 379 sucs = (data[o1:o2],)
380 380 else:
381 381 o2 = o1 + nodesize * numsuc
382 382 sucs = unpack(nodefmt * numsuc, data[o1:o2])
383 383
384 384 # read parents
385 385 if numpar == noneflag:
386 386 o3 = o2
387 387 parents = None
388 388 elif numpar == 1:
389 389 o3 = o2 + nodesize
390 390 parents = (data[o2:o3],)
391 391 else:
392 392 o3 = o2 + nodesize * numpar
393 393 parents = unpack(nodefmt * numpar, data[o2:o3])
394 394
395 395 # read metadata
396 396 off = o3 + metasize * nummeta
397 397 metapairsize = unpack(b'>' + (metafmt * nummeta), data[o3:off])
398 398 metadata = []
399 399 for idx in range(0, len(metapairsize), 2):
400 400 o1 = off + metapairsize[idx]
401 401 o2 = o1 + metapairsize[idx + 1]
402 402 metadata.append((data[off:o1], data[o1:o2]))
403 403 off = o2
404 404
405 405 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
406 406
407 407
408 408 def _fm1encodeonemarker(marker):
409 409 pre, sucs, flags, metadata, date, parents = marker
410 410 # determine node size
411 411 _fm1node = _fm1nodesha1
412 412 if flags & usingsha256:
413 413 _fm1node = _fm1nodesha256
414 414 numsuc = len(sucs)
415 415 numextranodes = 1 + numsuc
416 416 if parents is None:
417 417 numpar = _fm1parentnone
418 418 else:
419 419 numpar = len(parents)
420 420 numextranodes += numpar
421 421 formatnodes = _fm1node * numextranodes
422 422 formatmeta = _fm1metapair * len(metadata)
423 423 format = _fm1fixed + formatnodes + formatmeta
424 424 # tz is stored in minutes so we divide by 60
425 425 tz = date[1] // 60
426 426 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
427 427 data.extend(sucs)
428 428 if parents is not None:
429 429 data.extend(parents)
430 430 totalsize = _calcsize(format)
431 431 for key, value in metadata:
432 432 lk = len(key)
433 433 lv = len(value)
434 434 if lk > 255:
435 435 msg = (
436 436 b'obsstore metadata key cannot be longer than 255 bytes'
437 437 b' (key "%s" is %u bytes)'
438 438 ) % (key, lk)
439 439 raise error.ProgrammingError(msg)
440 440 if lv > 255:
441 441 msg = (
442 442 b'obsstore metadata value cannot be longer than 255 bytes'
443 443 b' (value "%s" for key "%s" is %u bytes)'
444 444 ) % (value, key, lv)
445 445 raise error.ProgrammingError(msg)
446 446 data.append(lk)
447 447 data.append(lv)
448 448 totalsize += lk + lv
449 449 data[0] = totalsize
450 450 data = [_pack(format, *data)]
451 451 for key, value in metadata:
452 452 data.append(key)
453 453 data.append(value)
454 454 return b''.join(data)
455 455
456 456
457 457 def _fm1readmarkers(data, off, stop):
458 458 native = getattr(parsers, 'fm1readmarkers', None)
459 459 if not native:
460 460 return _fm1purereadmarkers(data, off, stop)
461 461 return native(data, off, stop)
462 462
463 463
464 464 # mapping to read/write various marker formats
465 465 # <version> -> (decoder, encoder)
466 466 formats = {
467 467 _fm0version: (_fm0readmarkers, _fm0encodeonemarker),
468 468 _fm1version: (_fm1readmarkers, _fm1encodeonemarker),
469 469 }
470 470
471 471
472 472 def _readmarkerversion(data):
473 473 return _unpack(b'>B', data[0:1])[0]
474 474
475 475
476 476 @util.nogc
477 477 def _readmarkers(data, off=None, stop=None):
478 478 """Read and enumerate markers from raw data"""
479 479 diskversion = _readmarkerversion(data)
480 480 if not off:
481 481 off = 1 # skip 1 byte version number
482 482 if stop is None:
483 483 stop = len(data)
484 484 if diskversion not in formats:
485 485 msg = _(b'parsing obsolete marker: unknown version %r') % diskversion
486 486 raise error.UnknownVersion(msg, version=diskversion)
487 487 return diskversion, formats[diskversion][0](data, off, stop)
488 488
489 489
490 490 def encodeheader(version=_fm0version):
491 491 return _pack(b'>B', version)
492 492
493 493
494 494 def encodemarkers(markers, addheader=False, version=_fm0version):
495 495 # Kept separate from flushmarkers(), it will be reused for
496 496 # markers exchange.
497 497 encodeone = formats[version][1]
498 498 if addheader:
499 499 yield encodeheader(version)
500 500 for marker in markers:
501 501 yield encodeone(marker)
502 502
503 503
504 504 @util.nogc
505 505 def _addsuccessors(successors, markers):
506 506 for mark in markers:
507 507 successors.setdefault(mark[0], set()).add(mark)
508 508
509 509
510 510 @util.nogc
511 511 def _addpredecessors(predecessors, markers):
512 512 for mark in markers:
513 513 for suc in mark[1]:
514 514 predecessors.setdefault(suc, set()).add(mark)
515 515
516 516
517 517 @util.nogc
518 518 def _addchildren(children, markers):
519 519 for mark in markers:
520 520 parents = mark[5]
521 521 if parents is not None:
522 522 for p in parents:
523 523 children.setdefault(p, set()).add(mark)
524 524
525 525
526 526 def _checkinvalidmarkers(repo, markers):
527 527 """search for marker with invalid data and raise error if needed
528 528
529 529 Exist as a separated function to allow the evolve extension for a more
530 530 subtle handling.
531 531 """
532 532 for mark in markers:
533 533 if repo.nullid in mark[1]:
534 534 raise error.Abort(
535 535 _(
536 536 b'bad obsolescence marker detected: '
537 537 b'invalid successors nullid'
538 538 )
539 539 )
540 540
541 541
542 542 class obsstore:
543 543 """Store obsolete markers
544 544
545 545 Markers can be accessed with two mappings:
546 546 - predecessors[x] -> set(markers on predecessors edges of x)
547 547 - successors[x] -> set(markers on successors edges of x)
548 548 - children[x] -> set(markers on predecessors edges of children(x)
549 549 """
550 550
551 551 fields = (b'prec', b'succs', b'flag', b'meta', b'date', b'parents')
552 552 # prec: nodeid, predecessors changesets
553 553 # succs: tuple of nodeid, successor changesets (0-N length)
554 554 # flag: integer, flag field carrying modifier for the markers (see doc)
555 555 # meta: binary blob in UTF-8, encoded metadata dictionary
556 556 # date: (float, int) tuple, date of marker creation
557 557 # parents: (tuple of nodeid) or None, parents of predecessors
558 558 # None is used when no data has been recorded
559 559
560 560 def __init__(self, repo, svfs, defaultformat=_fm1version, readonly=False):
561 561 # caches for various obsolescence related cache
562 562 self.caches = {}
563 563 self.svfs = svfs
564 564 self._repo = weakref.ref(repo)
565 565 self._defaultformat = defaultformat
566 566 self._readonly = readonly
567 567
568 568 @property
569 569 def repo(self):
570 570 r = self._repo()
571 571 if r is None:
572 572 msg = "using the obsstore of a deallocated repo"
573 573 raise error.ProgrammingError(msg)
574 574 return r
575 575
576 576 def __iter__(self):
577 577 return iter(self._all)
578 578
579 579 def __len__(self):
580 580 return len(self._all)
581 581
582 582 def __nonzero__(self):
583 583 from . import statichttprepo
584 584
585 585 if isinstance(self.repo, statichttprepo.statichttprepository):
586 586 # If repo is accessed via static HTTP, then we can't use os.stat()
587 587 # to just peek at the file size.
588 588 return len(self._data) > 1
589 589 if not self._cached('_all'):
590 590 try:
591 591 return self.svfs.stat(b'obsstore').st_size > 1
592 592 except FileNotFoundError:
593 593 # just build an empty _all list if no obsstore exists, which
594 594 # avoids further stat() syscalls
595 595 pass
596 596 return bool(self._all)
597 597
598 598 __bool__ = __nonzero__
599 599
600 600 @property
601 601 def readonly(self):
602 602 """True if marker creation is disabled
603 603
604 604 Remove me in the future when obsolete marker is always on."""
605 605 return self._readonly
606 606
607 607 def create(
608 608 self,
609 609 transaction,
610 610 prec,
611 611 succs=(),
612 612 flag=0,
613 613 parents=None,
614 614 date=None,
615 615 metadata=None,
616 616 ui=None,
617 617 ):
618 618 """obsolete: add a new obsolete marker
619 619
620 620 * ensuring it is hashable
621 621 * check mandatory metadata
622 622 * encode metadata
623 623
624 624 If you are a human writing code creating marker you want to use the
625 625 `createmarkers` function in this module instead.
626 626
627 627 return True if a new marker have been added, False if the markers
628 628 already existed (no op).
629 629 """
630 630 flag = int(flag)
631 631 if metadata is None:
632 632 metadata = {}
633 633 if date is None:
634 634 if b'date' in metadata:
635 635 # as a courtesy for out-of-tree extensions
636 636 date = dateutil.parsedate(metadata.pop(b'date'))
637 637 elif ui is not None:
638 638 date = ui.configdate(b'devel', b'default-date')
639 639 if date is None:
640 640 date = dateutil.makedate()
641 641 else:
642 642 date = dateutil.makedate()
643 643 if flag & usingsha256:
644 644 if len(prec) != 32:
645 645 raise ValueError(prec)
646 646 for succ in succs:
647 647 if len(succ) != 32:
648 648 raise ValueError(succ)
649 649 else:
650 650 if len(prec) != 20:
651 651 raise ValueError(prec)
652 652 for succ in succs:
653 653 if len(succ) != 20:
654 654 raise ValueError(succ)
655 655 if prec in succs:
656 656 raise ValueError('in-marker cycle with %s' % prec.hex())
657 657
658 658 metadata = tuple(sorted(metadata.items()))
659 659 for k, v in metadata:
660 660 try:
661 661 # might be better to reject non-ASCII keys
662 662 k.decode('utf-8')
663 663 v.decode('utf-8')
664 664 except UnicodeDecodeError:
665 665 raise error.ProgrammingError(
666 666 b'obsstore metadata must be valid UTF-8 sequence '
667 667 b'(key = %r, value = %r)'
668 668 % (pycompat.bytestr(k), pycompat.bytestr(v))
669 669 )
670 670
671 671 marker = (bytes(prec), tuple(succs), flag, metadata, date, parents)
672 672 return bool(self.add(transaction, [marker]))
673 673
674 674 def add(self, transaction, markers):
675 675 """Add new markers to the store
676 676
677 677 Take care of filtering duplicate.
678 678 Return the number of new marker."""
679 679 if self._readonly:
680 680 raise error.Abort(
681 681 _(b'creating obsolete markers is not enabled on this repo')
682 682 )
683 683 known = set()
684 684 getsuccessors = self.successors.get
685 685 new = []
686 686 for m in markers:
687 687 if m not in getsuccessors(m[0], ()) and m not in known:
688 688 known.add(m)
689 689 new.append(m)
690 690 if new:
691 691 f = self.svfs(b'obsstore', b'ab')
692 692 try:
693 693 offset = f.tell()
694 694 transaction.add(b'obsstore', offset)
695 695 # offset == 0: new file - add the version header
696 696 data = b''.join(encodemarkers(new, offset == 0, self._version))
697 697 f.write(data)
698 698 finally:
699 699 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
700 700 # call 'filecacheentry.refresh()' here
701 701 f.close()
702 702 addedmarkers = transaction.changes.get(b'obsmarkers')
703 703 if addedmarkers is not None:
704 704 addedmarkers.update(new)
705 705 self._addmarkers(new, data)
706 706 # new marker *may* have changed several set. invalidate the cache.
707 707 self.caches.clear()
708 708 # records the number of new markers for the transaction hooks
709 709 previous = int(transaction.hookargs.get(b'new_obsmarkers', b'0'))
710 710 transaction.hookargs[b'new_obsmarkers'] = b'%d' % (previous + len(new))
711 711 return len(new)
712 712
713 713 def mergemarkers(self, transaction, data):
714 714 """merge a binary stream of markers inside the obsstore
715 715
716 716 Returns the number of new markers added."""
717 717 version, markers = _readmarkers(data)
718 718 return self.add(transaction, markers)
719 719
720 720 @propertycache
721 721 def _data(self):
722 722 return self.svfs.tryread(b'obsstore')
723 723
724 724 @propertycache
725 725 def _version(self):
726 726 if len(self._data) >= 1:
727 727 return _readmarkerversion(self._data)
728 728 else:
729 729 return self._defaultformat
730 730
731 731 @propertycache
732 732 def _all(self):
733 733 data = self._data
734 734 if not data:
735 735 return []
736 736 self._version, markers = _readmarkers(data)
737 737 markers = list(markers)
738 738 _checkinvalidmarkers(self.repo, markers)
739 739 return markers
740 740
741 741 @propertycache
742 742 def successors(self):
743 743 successors = {}
744 744 _addsuccessors(successors, self._all)
745 745 return successors
746 746
747 747 @propertycache
748 748 def predecessors(self):
749 749 predecessors = {}
750 750 _addpredecessors(predecessors, self._all)
751 751 return predecessors
752 752
753 753 @propertycache
754 754 def children(self):
755 755 children = {}
756 756 _addchildren(children, self._all)
757 757 return children
758 758
759 759 def _cached(self, attr):
760 760 return attr in self.__dict__
761 761
762 762 def _addmarkers(self, markers, rawdata):
763 763 markers = list(markers) # to allow repeated iteration
764 764 self._data = self._data + rawdata
765 765 self._all.extend(markers)
766 766 if self._cached('successors'):
767 767 _addsuccessors(self.successors, markers)
768 768 if self._cached('predecessors'):
769 769 _addpredecessors(self.predecessors, markers)
770 770 if self._cached('children'):
771 771 _addchildren(self.children, markers)
772 772 _checkinvalidmarkers(self.repo, markers)
773 773
774 774 def relevantmarkers(self, nodes=None, revs=None):
775 775 """return a set of all obsolescence markers relevant to a set of
776 776 nodes or revisions.
777 777
778 778 "relevant" to a set of nodes or revisions mean:
779 779
780 780 - marker that use this changeset as successor
781 781 - prune marker of direct children on this changeset
782 782 - recursive application of the two rules on predecessors of these
783 783 markers
784 784
785 785 It is a set so you cannot rely on order."""
786 786 if nodes is None:
787 787 nodes = set()
788 788 if revs is None:
789 789 revs = set()
790 790
791 791 get_rev = self.repo.unfiltered().changelog.index.get_rev
792 792 pendingnodes = set()
793 793 for marker in self._all:
794 794 for node in (marker[0],) + marker[1] + (marker[5] or ()):
795 795 if node in nodes:
796 796 pendingnodes.add(node)
797 797 elif revs:
798 798 rev = get_rev(node)
799 799 if rev is not None and rev in revs:
800 800 pendingnodes.add(node)
801 801 seenmarkers = set()
802 seenmarkers = set()
803 seennodes = set()
802 seennodes = set(pendingnodes)
804 803 precursorsmarkers = self.predecessors
805 804 succsmarkers = self.successors
806 805 children = self.children
807 806 while pendingnodes:
808 807 direct = set()
809 808 for current in pendingnodes:
810 809 direct.update(precursorsmarkers.get(current, ()))
811 810 pruned = [m for m in children.get(current, ()) if not m[1]]
812 811 direct.update(pruned)
813 812 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
814 813 direct.update(pruned)
815 814 direct -= seenmarkers
816 815 pendingnodes = {m[0] for m in direct}
817 816 seenmarkers |= direct
818 817 pendingnodes -= seennodes
819 818 seennodes |= pendingnodes
820 819 return seenmarkers
821 820
822 821
823 822 def makestore(ui, repo):
824 823 """Create an obsstore instance from a repo."""
825 824 # read default format for new obsstore.
826 825 # developer config: format.obsstore-version
827 826 defaultformat = ui.configint(b'format', b'obsstore-version')
828 827 # rely on obsstore class default when possible.
829 828 kwargs = {}
830 829 if defaultformat is not None:
831 830 kwargs['defaultformat'] = defaultformat
832 831 readonly = not isenabled(repo, createmarkersopt)
833 832 store = obsstore(repo, repo.svfs, readonly=readonly, **kwargs)
834 833 if store and readonly:
835 834 ui.warn(
836 835 _(b'"obsolete" feature not enabled but %i markers found!\n')
837 836 % len(list(store))
838 837 )
839 838 return store
840 839
841 840
842 841 def commonversion(versions):
843 842 """Return the newest version listed in both versions and our local formats.
844 843
845 844 Returns None if no common version exists.
846 845 """
847 846 versions.sort(reverse=True)
848 847 # search for highest version known on both side
849 848 for v in versions:
850 849 if v in formats:
851 850 return v
852 851 return None
853 852
854 853
855 854 # arbitrary picked to fit into 8K limit from HTTP server
856 855 # you have to take in account:
857 856 # - the version header
858 857 # - the base85 encoding
859 858 _maxpayload = 5300
860 859
861 860
862 861 def _pushkeyescape(markers):
863 862 """encode markers into a dict suitable for pushkey exchange
864 863
865 864 - binary data is base85 encoded
866 865 - split in chunks smaller than 5300 bytes"""
867 866 keys = {}
868 867 parts = []
869 868 currentlen = _maxpayload * 2 # ensure we create a new part
870 869 for marker in markers:
871 870 nextdata = _fm0encodeonemarker(marker)
872 871 if len(nextdata) + currentlen > _maxpayload:
873 872 currentpart = []
874 873 currentlen = 0
875 874 parts.append(currentpart)
876 875 currentpart.append(nextdata)
877 876 currentlen += len(nextdata)
878 877 for idx, part in enumerate(reversed(parts)):
879 878 data = b''.join([_pack(b'>B', _fm0version)] + part)
880 879 keys[b'dump%i' % idx] = util.b85encode(data)
881 880 return keys
882 881
883 882
884 883 def listmarkers(repo):
885 884 """List markers over pushkey"""
886 885 if not repo.obsstore:
887 886 return {}
888 887 return _pushkeyescape(sorted(repo.obsstore))
889 888
890 889
891 890 def pushmarker(repo, key, old, new):
892 891 """Push markers over pushkey"""
893 892 if not key.startswith(b'dump'):
894 893 repo.ui.warn(_(b'unknown key: %r') % key)
895 894 return False
896 895 if old:
897 896 repo.ui.warn(_(b'unexpected old value for %r') % key)
898 897 return False
899 898 data = util.b85decode(new)
900 899 with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr:
901 900 repo.obsstore.mergemarkers(tr, data)
902 901 repo.invalidatevolatilesets()
903 902 return True
904 903
905 904
906 905 # mapping of 'set-name' -> <function to compute this set>
907 906 cachefuncs = {}
908 907
909 908
910 909 def cachefor(name):
911 910 """Decorator to register a function as computing the cache for a set"""
912 911
913 912 def decorator(func):
914 913 if name in cachefuncs:
915 914 msg = b"duplicated registration for volatileset '%s' (existing: %r)"
916 915 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
917 916 cachefuncs[name] = func
918 917 return func
919 918
920 919 return decorator
921 920
922 921
923 922 def getrevs(repo, name):
924 923 """Return the set of revision that belong to the <name> set
925 924
926 925 Such access may compute the set and cache it for future use"""
927 926 repo = repo.unfiltered()
928 927 with util.timedcm('getrevs %s', name):
929 928 if not repo.obsstore:
930 929 return frozenset()
931 930 if name not in repo.obsstore.caches:
932 931 repo.obsstore.caches[name] = cachefuncs[name](repo)
933 932 return repo.obsstore.caches[name]
934 933
935 934
936 935 # To be simple we need to invalidate obsolescence cache when:
937 936 #
938 937 # - new changeset is added:
939 938 # - public phase is changed
940 939 # - obsolescence marker are added
941 940 # - strip is used a repo
942 941 def clearobscaches(repo):
943 942 """Remove all obsolescence related cache from a repo
944 943
945 944 This remove all cache in obsstore is the obsstore already exist on the
946 945 repo.
947 946
948 947 (We could be smarter here given the exact event that trigger the cache
949 948 clearing)"""
950 949 # only clear cache is there is obsstore data in this repo
951 950 if b'obsstore' in repo._filecache:
952 951 repo.obsstore.caches.clear()
953 952
954 953
955 954 def _mutablerevs(repo):
956 955 """the set of mutable revision in the repository"""
957 956 return repo._phasecache.getrevset(repo, phases.relevant_mutable_phases)
958 957
959 958
960 959 @cachefor(b'obsolete')
961 960 def _computeobsoleteset(repo):
962 961 """the set of obsolete revisions"""
963 962 getnode = repo.changelog.node
964 963 notpublic = _mutablerevs(repo)
965 964 isobs = repo.obsstore.successors.__contains__
966 965 return frozenset(r for r in notpublic if isobs(getnode(r)))
967 966
968 967
969 968 @cachefor(b'orphan')
970 969 def _computeorphanset(repo):
971 970 """the set of non obsolete revisions with obsolete parents"""
972 971 pfunc = repo.changelog.parentrevs
973 972 mutable = _mutablerevs(repo)
974 973 obsolete = getrevs(repo, b'obsolete')
975 974 others = mutable - obsolete
976 975 unstable = set()
977 976 for r in sorted(others):
978 977 # A rev is unstable if one of its parent is obsolete or unstable
979 978 # this works since we traverse following growing rev order
980 979 for p in pfunc(r):
981 980 if p in obsolete or p in unstable:
982 981 unstable.add(r)
983 982 break
984 983 return frozenset(unstable)
985 984
986 985
987 986 @cachefor(b'suspended')
988 987 def _computesuspendedset(repo):
989 988 """the set of obsolete parents with non obsolete descendants"""
990 989 suspended = repo.changelog.ancestors(getrevs(repo, b'orphan'))
991 990 return frozenset(r for r in getrevs(repo, b'obsolete') if r in suspended)
992 991
993 992
994 993 @cachefor(b'extinct')
995 994 def _computeextinctset(repo):
996 995 """the set of obsolete parents without non obsolete descendants"""
997 996 return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended')
998 997
999 998
1000 999 @cachefor(b'phasedivergent')
1001 1000 def _computephasedivergentset(repo):
1002 1001 """the set of revs trying to obsolete public revisions"""
1003 1002 bumped = set()
1004 1003 # util function (avoid attribute lookup in the loop)
1005 1004 phase = repo._phasecache.phase # would be faster to grab the full list
1006 1005 public = phases.public
1007 1006 cl = repo.changelog
1008 1007 torev = cl.index.get_rev
1009 1008 tonode = cl.node
1010 1009 obsstore = repo.obsstore
1011 1010 candidates = sorted(_mutablerevs(repo) - getrevs(repo, b"obsolete"))
1012 1011 for rev in candidates:
1013 1012 # We only evaluate mutable, non-obsolete revision
1014 1013 node = tonode(rev)
1015 1014 # (future) A cache of predecessors may worth if split is very common
1016 1015 for pnode in obsutil.allpredecessors(
1017 1016 obsstore, [node], ignoreflags=bumpedfix
1018 1017 ):
1019 1018 prev = torev(pnode) # unfiltered! but so is phasecache
1020 1019 if (prev is not None) and (phase(repo, prev) <= public):
1021 1020 # we have a public predecessor
1022 1021 bumped.add(rev)
1023 1022 break # Next draft!
1024 1023 return frozenset(bumped)
1025 1024
1026 1025
1027 1026 @cachefor(b'contentdivergent')
1028 1027 def _computecontentdivergentset(repo):
1029 1028 """the set of rev that compete to be the final successors of some revision."""
1030 1029 divergent = set()
1031 1030 obsstore = repo.obsstore
1032 1031 newermap = {}
1033 1032 tonode = repo.changelog.node
1034 1033 candidates = sorted(_mutablerevs(repo) - getrevs(repo, b"obsolete"))
1035 1034 for rev in candidates:
1036 1035 node = tonode(rev)
1037 1036 mark = obsstore.predecessors.get(node, ())
1038 1037 toprocess = set(mark)
1039 1038 seen = set()
1040 1039 while toprocess:
1041 1040 prec = toprocess.pop()[0]
1042 1041 if prec in seen:
1043 1042 continue # emergency cycle hanging prevention
1044 1043 seen.add(prec)
1045 1044 if prec not in newermap:
1046 1045 obsutil.successorssets(repo, prec, cache=newermap)
1047 1046 newer = [n for n in newermap[prec] if n]
1048 1047 if len(newer) > 1:
1049 1048 divergent.add(rev)
1050 1049 break
1051 1050 toprocess.update(obsstore.predecessors.get(prec, ()))
1052 1051 return frozenset(divergent)
1053 1052
1054 1053
1055 1054 def makefoldid(relation, user):
1056 1055
1057 1056 folddigest = hashutil.sha1(user)
1058 1057 for p in relation[0] + relation[1]:
1059 1058 folddigest.update(b'%d' % p.rev())
1060 1059 folddigest.update(p.node())
1061 1060 # Since fold only has to compete against fold for the same successors, it
1062 1061 # seems fine to use a small ID. Smaller ID save space.
1063 1062 return hex(folddigest.digest())[:8]
1064 1063
1065 1064
1066 1065 def createmarkers(
1067 1066 repo, relations, flag=0, date=None, metadata=None, operation=None
1068 1067 ):
1069 1068 """Add obsolete markers between changesets in a repo
1070 1069
1071 1070 <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}])
1072 1071 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1073 1072 containing metadata for this marker only. It is merged with the global
1074 1073 metadata specified through the `metadata` argument of this function.
1075 1074 Any string values in metadata must be UTF-8 bytes.
1076 1075
1077 1076 Trying to obsolete a public changeset will raise an exception.
1078 1077
1079 1078 Current user and date are used except if specified otherwise in the
1080 1079 metadata attribute.
1081 1080
1082 1081 This function operates within a transaction of its own, but does
1083 1082 not take any lock on the repo.
1084 1083 """
1085 1084 # prepare metadata
1086 1085 if metadata is None:
1087 1086 metadata = {}
1088 1087 if b'user' not in metadata:
1089 1088 luser = (
1090 1089 repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username()
1091 1090 )
1092 1091 metadata[b'user'] = encoding.fromlocal(luser)
1093 1092
1094 1093 # Operation metadata handling
1095 1094 useoperation = repo.ui.configbool(
1096 1095 b'experimental', b'evolution.track-operation'
1097 1096 )
1098 1097 if useoperation and operation:
1099 1098 metadata[b'operation'] = operation
1100 1099
1101 1100 # Effect flag metadata handling
1102 1101 saveeffectflag = repo.ui.configbool(
1103 1102 b'experimental', b'evolution.effect-flags'
1104 1103 )
1105 1104
1106 1105 with repo.transaction(b'add-obsolescence-marker') as tr:
1107 1106 markerargs = []
1108 1107 for rel in relations:
1109 1108 predecessors = rel[0]
1110 1109 if not isinstance(predecessors, tuple):
1111 1110 # preserve compat with old API until all caller are migrated
1112 1111 predecessors = (predecessors,)
1113 1112 if len(predecessors) > 1 and len(rel[1]) != 1:
1114 1113 msg = b'Fold markers can only have 1 successors, not %d'
1115 1114 raise error.ProgrammingError(msg % len(rel[1]))
1116 1115 foldid = None
1117 1116 foldsize = len(predecessors)
1118 1117 if 1 < foldsize:
1119 1118 foldid = makefoldid(rel, metadata[b'user'])
1120 1119 for foldidx, prec in enumerate(predecessors, 1):
1121 1120 sucs = rel[1]
1122 1121 localmetadata = metadata.copy()
1123 1122 if len(rel) > 2:
1124 1123 localmetadata.update(rel[2])
1125 1124 if foldid is not None:
1126 1125 localmetadata[b'fold-id'] = foldid
1127 1126 localmetadata[b'fold-idx'] = b'%d' % foldidx
1128 1127 localmetadata[b'fold-size'] = b'%d' % foldsize
1129 1128
1130 1129 if not prec.mutable():
1131 1130 raise error.Abort(
1132 1131 _(b"cannot obsolete public changeset: %s") % prec,
1133 1132 hint=b"see 'hg help phases' for details",
1134 1133 )
1135 1134 nprec = prec.node()
1136 1135 nsucs = tuple(s.node() for s in sucs)
1137 1136 npare = None
1138 1137 if not nsucs:
1139 1138 npare = tuple(p.node() for p in prec.parents())
1140 1139 if nprec in nsucs:
1141 1140 raise error.Abort(
1142 1141 _(b"changeset %s cannot obsolete itself") % prec
1143 1142 )
1144 1143
1145 1144 # Effect flag can be different by relation
1146 1145 if saveeffectflag:
1147 1146 # The effect flag is saved in a versioned field name for
1148 1147 # future evolution
1149 1148 effectflag = obsutil.geteffectflag(prec, sucs)
1150 1149 localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag
1151 1150
1152 1151 # Creating the marker causes the hidden cache to become
1153 1152 # invalid, which causes recomputation when we ask for
1154 1153 # prec.parents() above. Resulting in n^2 behavior. So let's
1155 1154 # prepare all of the args first, then create the markers.
1156 1155 markerargs.append((nprec, nsucs, npare, localmetadata))
1157 1156
1158 1157 for args in markerargs:
1159 1158 nprec, nsucs, npare, localmetadata = args
1160 1159 repo.obsstore.create(
1161 1160 tr,
1162 1161 nprec,
1163 1162 nsucs,
1164 1163 flag,
1165 1164 parents=npare,
1166 1165 date=date,
1167 1166 metadata=localmetadata,
1168 1167 ui=repo.ui,
1169 1168 )
1170 1169 repo.filteredrevcache.clear()
General Comments 0
You need to be logged in to leave comments. Login now