##// END OF EJS Templates
unstable: use the `_mutablerevs` function when computing phase divergent...
marmoute -
r52400:5f9af842 default
parent child Browse files
Show More
@@ -1,1153 +1,1154 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):
775 775 """return a set of all obsolescence markers relevant to a set of nodes.
776 776
777 777 "relevant" to a set of nodes mean:
778 778
779 779 - marker that use this changeset as successor
780 780 - prune marker of direct children on this changeset
781 781 - recursive application of the two rules on predecessors of these
782 782 markers
783 783
784 784 It is a set so you cannot rely on order."""
785 785
786 786 pendingnodes = set(nodes)
787 787 seenmarkers = set()
788 788 seennodes = set(pendingnodes)
789 789 precursorsmarkers = self.predecessors
790 790 succsmarkers = self.successors
791 791 children = self.children
792 792 while pendingnodes:
793 793 direct = set()
794 794 for current in pendingnodes:
795 795 direct.update(precursorsmarkers.get(current, ()))
796 796 pruned = [m for m in children.get(current, ()) if not m[1]]
797 797 direct.update(pruned)
798 798 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
799 799 direct.update(pruned)
800 800 direct -= seenmarkers
801 801 pendingnodes = {m[0] for m in direct}
802 802 seenmarkers |= direct
803 803 pendingnodes -= seennodes
804 804 seennodes |= pendingnodes
805 805 return seenmarkers
806 806
807 807
808 808 def makestore(ui, repo):
809 809 """Create an obsstore instance from a repo."""
810 810 # read default format for new obsstore.
811 811 # developer config: format.obsstore-version
812 812 defaultformat = ui.configint(b'format', b'obsstore-version')
813 813 # rely on obsstore class default when possible.
814 814 kwargs = {}
815 815 if defaultformat is not None:
816 816 kwargs['defaultformat'] = defaultformat
817 817 readonly = not isenabled(repo, createmarkersopt)
818 818 store = obsstore(repo, repo.svfs, readonly=readonly, **kwargs)
819 819 if store and readonly:
820 820 ui.warn(
821 821 _(b'obsolete feature not enabled but %i markers found!\n')
822 822 % len(list(store))
823 823 )
824 824 return store
825 825
826 826
827 827 def commonversion(versions):
828 828 """Return the newest version listed in both versions and our local formats.
829 829
830 830 Returns None if no common version exists.
831 831 """
832 832 versions.sort(reverse=True)
833 833 # search for highest version known on both side
834 834 for v in versions:
835 835 if v in formats:
836 836 return v
837 837 return None
838 838
839 839
840 840 # arbitrary picked to fit into 8K limit from HTTP server
841 841 # you have to take in account:
842 842 # - the version header
843 843 # - the base85 encoding
844 844 _maxpayload = 5300
845 845
846 846
847 847 def _pushkeyescape(markers):
848 848 """encode markers into a dict suitable for pushkey exchange
849 849
850 850 - binary data is base85 encoded
851 851 - split in chunks smaller than 5300 bytes"""
852 852 keys = {}
853 853 parts = []
854 854 currentlen = _maxpayload * 2 # ensure we create a new part
855 855 for marker in markers:
856 856 nextdata = _fm0encodeonemarker(marker)
857 857 if len(nextdata) + currentlen > _maxpayload:
858 858 currentpart = []
859 859 currentlen = 0
860 860 parts.append(currentpart)
861 861 currentpart.append(nextdata)
862 862 currentlen += len(nextdata)
863 863 for idx, part in enumerate(reversed(parts)):
864 864 data = b''.join([_pack(b'>B', _fm0version)] + part)
865 865 keys[b'dump%i' % idx] = util.b85encode(data)
866 866 return keys
867 867
868 868
869 869 def listmarkers(repo):
870 870 """List markers over pushkey"""
871 871 if not repo.obsstore:
872 872 return {}
873 873 return _pushkeyescape(sorted(repo.obsstore))
874 874
875 875
876 876 def pushmarker(repo, key, old, new):
877 877 """Push markers over pushkey"""
878 878 if not key.startswith(b'dump'):
879 879 repo.ui.warn(_(b'unknown key: %r') % key)
880 880 return False
881 881 if old:
882 882 repo.ui.warn(_(b'unexpected old value for %r') % key)
883 883 return False
884 884 data = util.b85decode(new)
885 885 with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr:
886 886 repo.obsstore.mergemarkers(tr, data)
887 887 repo.invalidatevolatilesets()
888 888 return True
889 889
890 890
891 891 # mapping of 'set-name' -> <function to compute this set>
892 892 cachefuncs = {}
893 893
894 894
895 895 def cachefor(name):
896 896 """Decorator to register a function as computing the cache for a set"""
897 897
898 898 def decorator(func):
899 899 if name in cachefuncs:
900 900 msg = b"duplicated registration for volatileset '%s' (existing: %r)"
901 901 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
902 902 cachefuncs[name] = func
903 903 return func
904 904
905 905 return decorator
906 906
907 907
908 908 def getrevs(repo, name):
909 909 """Return the set of revision that belong to the <name> set
910 910
911 911 Such access may compute the set and cache it for future use"""
912 912 repo = repo.unfiltered()
913 913 with util.timedcm('getrevs %s', name):
914 914 if not repo.obsstore:
915 915 return frozenset()
916 916 if name not in repo.obsstore.caches:
917 917 repo.obsstore.caches[name] = cachefuncs[name](repo)
918 918 return repo.obsstore.caches[name]
919 919
920 920
921 921 # To be simple we need to invalidate obsolescence cache when:
922 922 #
923 923 # - new changeset is added:
924 924 # - public phase is changed
925 925 # - obsolescence marker are added
926 926 # - strip is used a repo
927 927 def clearobscaches(repo):
928 928 """Remove all obsolescence related cache from a repo
929 929
930 930 This remove all cache in obsstore is the obsstore already exist on the
931 931 repo.
932 932
933 933 (We could be smarter here given the exact event that trigger the cache
934 934 clearing)"""
935 935 # only clear cache is there is obsstore data in this repo
936 936 if b'obsstore' in repo._filecache:
937 937 repo.obsstore.caches.clear()
938 938
939 939
940 940 def _mutablerevs(repo):
941 941 """the set of mutable revision in the repository"""
942 942 return repo._phasecache.getrevset(repo, phases.mutablephases)
943 943
944 944
945 945 @cachefor(b'obsolete')
946 946 def _computeobsoleteset(repo):
947 947 """the set of obsolete revisions"""
948 948 getnode = repo.changelog.node
949 949 notpublic = _mutablerevs(repo)
950 950 isobs = repo.obsstore.successors.__contains__
951 951 return frozenset(r for r in notpublic if isobs(getnode(r)))
952 952
953 953
954 954 @cachefor(b'orphan')
955 955 def _computeorphanset(repo):
956 956 """the set of non obsolete revisions with obsolete parents"""
957 957 pfunc = repo.changelog.parentrevs
958 958 mutable = _mutablerevs(repo)
959 959 obsolete = getrevs(repo, b'obsolete')
960 960 others = mutable - obsolete
961 961 unstable = set()
962 962 for r in sorted(others):
963 963 # A rev is unstable if one of its parent is obsolete or unstable
964 964 # this works since we traverse following growing rev order
965 965 for p in pfunc(r):
966 966 if p in obsolete or p in unstable:
967 967 unstable.add(r)
968 968 break
969 969 return frozenset(unstable)
970 970
971 971
972 972 @cachefor(b'suspended')
973 973 def _computesuspendedset(repo):
974 974 """the set of obsolete parents with non obsolete descendants"""
975 975 suspended = repo.changelog.ancestors(getrevs(repo, b'orphan'))
976 976 return frozenset(r for r in getrevs(repo, b'obsolete') if r in suspended)
977 977
978 978
979 979 @cachefor(b'extinct')
980 980 def _computeextinctset(repo):
981 981 """the set of obsolete parents without non obsolete descendants"""
982 982 return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended')
983 983
984 984
985 985 @cachefor(b'phasedivergent')
986 986 def _computephasedivergentset(repo):
987 987 """the set of revs trying to obsolete public revisions"""
988 988 bumped = set()
989 989 # util function (avoid attribute lookup in the loop)
990 990 phase = repo._phasecache.phase # would be faster to grab the full list
991 991 public = phases.public
992 992 cl = repo.changelog
993 993 torev = cl.index.get_rev
994 994 tonode = cl.node
995 995 obsstore = repo.obsstore
996 for rev in repo.revs(b'(not public()) and (not obsolete())'):
996 candidates = sorted(_mutablerevs(repo) - getrevs(repo, b"obsolete"))
997 for rev in candidates:
997 998 # We only evaluate mutable, non-obsolete revision
998 999 node = tonode(rev)
999 1000 # (future) A cache of predecessors may worth if split is very common
1000 1001 for pnode in obsutil.allpredecessors(
1001 1002 obsstore, [node], ignoreflags=bumpedfix
1002 1003 ):
1003 1004 prev = torev(pnode) # unfiltered! but so is phasecache
1004 1005 if (prev is not None) and (phase(repo, prev) <= public):
1005 1006 # we have a public predecessor
1006 1007 bumped.add(rev)
1007 1008 break # Next draft!
1008 1009 return frozenset(bumped)
1009 1010
1010 1011
1011 1012 @cachefor(b'contentdivergent')
1012 1013 def _computecontentdivergentset(repo):
1013 1014 """the set of rev that compete to be the final successors of some revision."""
1014 1015 divergent = set()
1015 1016 obsstore = repo.obsstore
1016 1017 newermap = {}
1017 1018 tonode = repo.changelog.node
1018 1019 for rev in repo.revs(b'(not public()) - obsolete()'):
1019 1020 node = tonode(rev)
1020 1021 mark = obsstore.predecessors.get(node, ())
1021 1022 toprocess = set(mark)
1022 1023 seen = set()
1023 1024 while toprocess:
1024 1025 prec = toprocess.pop()[0]
1025 1026 if prec in seen:
1026 1027 continue # emergency cycle hanging prevention
1027 1028 seen.add(prec)
1028 1029 if prec not in newermap:
1029 1030 obsutil.successorssets(repo, prec, cache=newermap)
1030 1031 newer = [n for n in newermap[prec] if n]
1031 1032 if len(newer) > 1:
1032 1033 divergent.add(rev)
1033 1034 break
1034 1035 toprocess.update(obsstore.predecessors.get(prec, ()))
1035 1036 return frozenset(divergent)
1036 1037
1037 1038
1038 1039 def makefoldid(relation, user):
1039 1040
1040 1041 folddigest = hashutil.sha1(user)
1041 1042 for p in relation[0] + relation[1]:
1042 1043 folddigest.update(b'%d' % p.rev())
1043 1044 folddigest.update(p.node())
1044 1045 # Since fold only has to compete against fold for the same successors, it
1045 1046 # seems fine to use a small ID. Smaller ID save space.
1046 1047 return hex(folddigest.digest())[:8]
1047 1048
1048 1049
1049 1050 def createmarkers(
1050 1051 repo, relations, flag=0, date=None, metadata=None, operation=None
1051 1052 ):
1052 1053 """Add obsolete markers between changesets in a repo
1053 1054
1054 1055 <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}])
1055 1056 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1056 1057 containing metadata for this marker only. It is merged with the global
1057 1058 metadata specified through the `metadata` argument of this function.
1058 1059 Any string values in metadata must be UTF-8 bytes.
1059 1060
1060 1061 Trying to obsolete a public changeset will raise an exception.
1061 1062
1062 1063 Current user and date are used except if specified otherwise in the
1063 1064 metadata attribute.
1064 1065
1065 1066 This function operates within a transaction of its own, but does
1066 1067 not take any lock on the repo.
1067 1068 """
1068 1069 # prepare metadata
1069 1070 if metadata is None:
1070 1071 metadata = {}
1071 1072 if b'user' not in metadata:
1072 1073 luser = (
1073 1074 repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username()
1074 1075 )
1075 1076 metadata[b'user'] = encoding.fromlocal(luser)
1076 1077
1077 1078 # Operation metadata handling
1078 1079 useoperation = repo.ui.configbool(
1079 1080 b'experimental', b'evolution.track-operation'
1080 1081 )
1081 1082 if useoperation and operation:
1082 1083 metadata[b'operation'] = operation
1083 1084
1084 1085 # Effect flag metadata handling
1085 1086 saveeffectflag = repo.ui.configbool(
1086 1087 b'experimental', b'evolution.effect-flags'
1087 1088 )
1088 1089
1089 1090 with repo.transaction(b'add-obsolescence-marker') as tr:
1090 1091 markerargs = []
1091 1092 for rel in relations:
1092 1093 predecessors = rel[0]
1093 1094 if not isinstance(predecessors, tuple):
1094 1095 # preserve compat with old API until all caller are migrated
1095 1096 predecessors = (predecessors,)
1096 1097 if len(predecessors) > 1 and len(rel[1]) != 1:
1097 1098 msg = b'Fold markers can only have 1 successors, not %d'
1098 1099 raise error.ProgrammingError(msg % len(rel[1]))
1099 1100 foldid = None
1100 1101 foldsize = len(predecessors)
1101 1102 if 1 < foldsize:
1102 1103 foldid = makefoldid(rel, metadata[b'user'])
1103 1104 for foldidx, prec in enumerate(predecessors, 1):
1104 1105 sucs = rel[1]
1105 1106 localmetadata = metadata.copy()
1106 1107 if len(rel) > 2:
1107 1108 localmetadata.update(rel[2])
1108 1109 if foldid is not None:
1109 1110 localmetadata[b'fold-id'] = foldid
1110 1111 localmetadata[b'fold-idx'] = b'%d' % foldidx
1111 1112 localmetadata[b'fold-size'] = b'%d' % foldsize
1112 1113
1113 1114 if not prec.mutable():
1114 1115 raise error.Abort(
1115 1116 _(b"cannot obsolete public changeset: %s") % prec,
1116 1117 hint=b"see 'hg help phases' for details",
1117 1118 )
1118 1119 nprec = prec.node()
1119 1120 nsucs = tuple(s.node() for s in sucs)
1120 1121 npare = None
1121 1122 if not nsucs:
1122 1123 npare = tuple(p.node() for p in prec.parents())
1123 1124 if nprec in nsucs:
1124 1125 raise error.Abort(
1125 1126 _(b"changeset %s cannot obsolete itself") % prec
1126 1127 )
1127 1128
1128 1129 # Effect flag can be different by relation
1129 1130 if saveeffectflag:
1130 1131 # The effect flag is saved in a versioned field name for
1131 1132 # future evolution
1132 1133 effectflag = obsutil.geteffectflag(prec, sucs)
1133 1134 localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag
1134 1135
1135 1136 # Creating the marker causes the hidden cache to become
1136 1137 # invalid, which causes recomputation when we ask for
1137 1138 # prec.parents() above. Resulting in n^2 behavior. So let's
1138 1139 # prepare all of the args first, then create the markers.
1139 1140 markerargs.append((nprec, nsucs, npare, localmetadata))
1140 1141
1141 1142 for args in markerargs:
1142 1143 nprec, nsucs, npare, localmetadata = args
1143 1144 repo.obsstore.create(
1144 1145 tr,
1145 1146 nprec,
1146 1147 nsucs,
1147 1148 flag,
1148 1149 parents=npare,
1149 1150 date=date,
1150 1151 metadata=localmetadata,
1151 1152 ui=repo.ui,
1152 1153 )
1153 1154 repo.filteredrevcache.clear()
General Comments 0
You need to be logged in to leave comments. Login now