##// END OF EJS Templates
obsolete: use uint## in the format documention...
Pierre-Yves David -
r22849:fd759142 default
parent child Browse files
Show More
@@ -1,1008 +1,1008 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 "precursor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a precursor 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 "precursor markers of Y" because they hold
28 28 information about the precursors 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 are used:
46 46
47 47 (A, (C, 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 import struct
71 71 import util, base85, node
72 72 import phases
73 73 from i18n import _
74 74
75 75 _pack = struct.pack
76 76 _unpack = struct.unpack
77 77
78 78 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
79 79
80 80 # the obsolete feature is not mature enough to be enabled by default.
81 81 # you have to rely on third party extension extension to enable this.
82 82 _enabled = False
83 83
84 84 ### obsolescence marker flag
85 85
86 86 ## bumpedfix flag
87 87 #
88 88 # When a changeset A' succeed to a changeset A which became public, we call A'
89 89 # "bumped" because it's a successors of a public changesets
90 90 #
91 91 # o A' (bumped)
92 92 # |`:
93 93 # | o A
94 94 # |/
95 95 # o Z
96 96 #
97 97 # The way to solve this situation is to create a new changeset Ad as children
98 98 # of A. This changeset have the same content than A'. So the diff from A to A'
99 99 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
100 100 #
101 101 # o Ad
102 102 # |`:
103 103 # | x A'
104 104 # |'|
105 105 # o | A
106 106 # |/
107 107 # o Z
108 108 #
109 109 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
110 110 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
111 111 # This flag mean that the successors express the changes between the public and
112 112 # bumped version and fix the situation, breaking the transitivity of
113 113 # "bumped" here.
114 114 bumpedfix = 1
115 115
116 116 ## Parsing and writing of version "0"
117 117 #
118 118 # The header is followed by the markers. Each marker is made of:
119 119 #
120 # - 1 unsigned byte: number of new changesets "N", can be zero.
120 # - 1 uint8 : number of new changesets "N", can be zero.
121 121 #
122 # - 1 unsigned 32-bits integer: metadata size "M" in bytes.
122 # - 1 uint32: metadata size "M" in bytes.
123 123 #
124 124 # - 1 byte: a bit field. It is reserved for flags used in common
125 125 # obsolete marker operations, to avoid repeated decoding of metadata
126 126 # entries.
127 127 #
128 128 # - 20 bytes: obsoleted changeset identifier.
129 129 #
130 130 # - N*20 bytes: new changesets identifiers.
131 131 #
132 132 # - M bytes: metadata as a sequence of nul-terminated strings. Each
133 133 # string contains a key and a value, separated by a colon ':', without
134 134 # additional encoding. Keys cannot contain '\0' or ':' and values
135 135 # cannot contain '\0'.
136 136 _fm0version = 0
137 137 _fm0fixed = '>BIB20s'
138 138 _fm0node = '20s'
139 139 _fm0fsize = struct.calcsize(_fm0fixed)
140 140 _fm0fnodesize = struct.calcsize(_fm0node)
141 141
142 142 def _fm0readmarkers(data, off=0):
143 143 # Loop on markers
144 144 l = len(data)
145 145 while off + _fm0fsize <= l:
146 146 # read fixed part
147 147 cur = data[off:off + _fm0fsize]
148 148 off += _fm0fsize
149 149 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
150 150 # read replacement
151 151 sucs = ()
152 152 if numsuc:
153 153 s = (_fm0fnodesize * numsuc)
154 154 cur = data[off:off + s]
155 155 sucs = _unpack(_fm0node * numsuc, cur)
156 156 off += s
157 157 # read metadata
158 158 # (metadata will be decoded on demand)
159 159 metadata = data[off:off + mdsize]
160 160 if len(metadata) != mdsize:
161 161 raise util.Abort(_('parsing obsolete marker: metadata is too '
162 162 'short, %d bytes expected, got %d')
163 163 % (mdsize, len(metadata)))
164 164 off += mdsize
165 165 metadata = _fm0decodemeta(metadata)
166 166 try:
167 167 when, offset = metadata.pop('date', '0 0').split(' ')
168 168 date = float(when), int(offset)
169 169 except ValueError:
170 170 date = (0., 0)
171 171 parents = None
172 172 if 'p2' in metadata:
173 173 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
174 174 elif 'p1' in metadata:
175 175 parents = (metadata.pop('p1', None),)
176 176 elif 'p0' in metadata:
177 177 parents = ()
178 178 if parents is not None:
179 179 try:
180 180 parents = tuple(node.bin(p) for p in parents)
181 181 # if parent content is not a nodeid, drop the data
182 182 for p in parents:
183 183 if len(p) != 20:
184 184 parents = None
185 185 break
186 186 except TypeError:
187 187 # if content cannot be translated to nodeid drop the data.
188 188 parents = None
189 189
190 190 metadata = tuple(sorted(metadata.iteritems()))
191 191
192 192 yield (pre, sucs, flags, metadata, date, parents)
193 193
194 194 def _fm0encodeonemarker(marker):
195 195 pre, sucs, flags, metadata, date, parents = marker
196 196 metadata = dict(metadata)
197 197 metadata['date'] = '%d %i' % date
198 198 if parents is not None:
199 199 if not parents:
200 200 # mark that we explicitly recorded no parents
201 201 metadata['p0'] = ''
202 202 for i, p in enumerate(parents):
203 203 metadata['p%i' % (i + 1)] = node.hex(p)
204 204 metadata = _fm0encodemeta(metadata)
205 205 numsuc = len(sucs)
206 206 format = _fm0fixed + (_fm0node * numsuc)
207 207 data = [numsuc, len(metadata), flags, pre]
208 208 data.extend(sucs)
209 209 return _pack(format, *data) + metadata
210 210
211 211 def _fm0encodemeta(meta):
212 212 """Return encoded metadata string to string mapping.
213 213
214 214 Assume no ':' in key and no '\0' in both key and value."""
215 215 for key, value in meta.iteritems():
216 216 if ':' in key or '\0' in key:
217 217 raise ValueError("':' and '\0' are forbidden in metadata key'")
218 218 if '\0' in value:
219 219 raise ValueError("':' is forbidden in metadata value'")
220 220 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
221 221
222 222 def _fm0decodemeta(data):
223 223 """Return string to string dictionary from encoded version."""
224 224 d = {}
225 225 for l in data.split('\0'):
226 226 if l:
227 227 key, value = l.split(':')
228 228 d[key] = value
229 229 return d
230 230
231 231
232 232 # mapping to read/write various marker formats
233 233 # <version> -> (decoder, encoder)
234 234 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker)}
235 235
236 236 def _readmarkers(data):
237 237 """Read and enumerate markers from raw data"""
238 238 off = 0
239 239 diskversion = _unpack('>B', data[off:off + 1])[0]
240 240 off += 1
241 241 if diskversion not in formats:
242 242 raise util.Abort(_('parsing obsolete marker: unknown version %r')
243 243 % diskversion)
244 244 return diskversion, formats[diskversion][0](data, off)
245 245
246 246 def encodemarkers(markers, addheader=False, version=_fm0version):
247 247 # Kept separate from flushmarkers(), it will be reused for
248 248 # markers exchange.
249 249 encodeone = formats[version][1]
250 250 if addheader:
251 251 yield _pack('>B', version)
252 252 for marker in markers:
253 253 yield encodeone(marker)
254 254
255 255
256 256 class marker(object):
257 257 """Wrap obsolete marker raw data"""
258 258
259 259 def __init__(self, repo, data):
260 260 # the repo argument will be used to create changectx in later version
261 261 self._repo = repo
262 262 self._data = data
263 263 self._decodedmeta = None
264 264
265 265 def __hash__(self):
266 266 return hash(self._data)
267 267
268 268 def __eq__(self, other):
269 269 if type(other) != type(self):
270 270 return False
271 271 return self._data == other._data
272 272
273 273 def precnode(self):
274 274 """Precursor changeset node identifier"""
275 275 return self._data[0]
276 276
277 277 def succnodes(self):
278 278 """List of successor changesets node identifiers"""
279 279 return self._data[1]
280 280
281 281 def parentnodes(self):
282 282 """Parents of the precursors (None if not recorded)"""
283 283 return self._data[5]
284 284
285 285 def metadata(self):
286 286 """Decoded metadata dictionary"""
287 287 return dict(self._data[3])
288 288
289 289 def date(self):
290 290 """Creation date as (unixtime, offset)"""
291 291 return self._data[4]
292 292
293 293 def flags(self):
294 294 """The flags field of the marker"""
295 295 return self._data[2]
296 296
297 297 class obsstore(object):
298 298 """Store obsolete markers
299 299
300 300 Markers can be accessed with two mappings:
301 301 - precursors[x] -> set(markers on precursors edges of x)
302 302 - successors[x] -> set(markers on successors edges of x)
303 303 - children[x] -> set(markers on precursors edges of children(x)
304 304 """
305 305
306 306 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
307 307 # prec: nodeid, precursor changesets
308 308 # succs: tuple of nodeid, successor changesets (0-N length)
309 309 # flag: integer, flag field carrying modifier for the markers (see doc)
310 310 # meta: binary blob, encoded metadata dictionary
311 311 # date: (float, int) tuple, date of marker creation
312 312 # parents: (tuple of nodeid) or None, parents of precursors
313 313 # None is used when no data has been recorded
314 314
315 315 def __init__(self, sopener):
316 316 # caches for various obsolescence related cache
317 317 self.caches = {}
318 318 self._all = []
319 319 self.precursors = {}
320 320 self.successors = {}
321 321 self.children = {}
322 322 self.sopener = sopener
323 323 data = sopener.tryread('obsstore')
324 324 self._version = _fm0version
325 325 if data:
326 326 self._version, markers = _readmarkers(data)
327 327 self._load(markers)
328 328
329 329 def __iter__(self):
330 330 return iter(self._all)
331 331
332 332 def __len__(self):
333 333 return len(self._all)
334 334
335 335 def __nonzero__(self):
336 336 return bool(self._all)
337 337
338 338 def create(self, transaction, prec, succs=(), flag=0, parents=None,
339 339 date=None, metadata=None):
340 340 """obsolete: add a new obsolete marker
341 341
342 342 * ensuring it is hashable
343 343 * check mandatory metadata
344 344 * encode metadata
345 345
346 346 If you are a human writing code creating marker you want to use the
347 347 `createmarkers` function in this module instead.
348 348
349 349 return True if a new marker have been added, False if the markers
350 350 already existed (no op).
351 351 """
352 352 if metadata is None:
353 353 metadata = {}
354 354 if date is None:
355 355 if 'date' in metadata:
356 356 # as a courtesy for out-of-tree extensions
357 357 date = util.parsedate(metadata.pop('date'))
358 358 else:
359 359 date = util.makedate()
360 360 if len(prec) != 20:
361 361 raise ValueError(prec)
362 362 for succ in succs:
363 363 if len(succ) != 20:
364 364 raise ValueError(succ)
365 365 if prec in succs:
366 366 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
367 367
368 368 metadata = tuple(sorted(metadata.iteritems()))
369 369
370 370 marker = (str(prec), tuple(succs), int(flag), metadata, date, parents)
371 371 return bool(self.add(transaction, [marker]))
372 372
373 373 def add(self, transaction, markers):
374 374 """Add new markers to the store
375 375
376 376 Take care of filtering duplicate.
377 377 Return the number of new marker."""
378 378 if not _enabled:
379 379 raise util.Abort('obsolete feature is not enabled on this repo')
380 380 known = set(self._all)
381 381 new = []
382 382 for m in markers:
383 383 if m not in known:
384 384 known.add(m)
385 385 new.append(m)
386 386 if new:
387 387 f = self.sopener('obsstore', 'ab')
388 388 try:
389 389 # Whether the file's current position is at the begin or at
390 390 # the end after opening a file for appending is implementation
391 391 # defined. So we must seek to the end before calling tell(),
392 392 # or we may get a zero offset for non-zero sized files on
393 393 # some platforms (issue3543).
394 394 f.seek(0, _SEEK_END)
395 395 offset = f.tell()
396 396 transaction.add('obsstore', offset)
397 397 # offset == 0: new file - add the version header
398 398 for bytes in encodemarkers(new, offset == 0, self._version):
399 399 f.write(bytes)
400 400 finally:
401 401 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
402 402 # call 'filecacheentry.refresh()' here
403 403 f.close()
404 404 self._load(new)
405 405 # new marker *may* have changed several set. invalidate the cache.
406 406 self.caches.clear()
407 407 # records the number of new markers for the transaction hooks
408 408 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
409 409 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
410 410 return len(new)
411 411
412 412 def mergemarkers(self, transaction, data):
413 413 """merge a binary stream of markers inside the obsstore
414 414
415 415 Returns the number of new markers added."""
416 416 version, markers = _readmarkers(data)
417 417 return self.add(transaction, markers)
418 418
419 419 def _load(self, markers):
420 420 for mark in markers:
421 421 self._all.append(mark)
422 422 pre, sucs = mark[:2]
423 423 self.successors.setdefault(pre, set()).add(mark)
424 424 for suc in sucs:
425 425 self.precursors.setdefault(suc, set()).add(mark)
426 426 parents = mark[5]
427 427 if parents is not None:
428 428 for p in parents:
429 429 self.children.setdefault(p, set()).add(mark)
430 430 if node.nullid in self.precursors:
431 431 raise util.Abort(_('bad obsolescence marker detected: '
432 432 'invalid successors nullid'))
433 433 def relevantmarkers(self, nodes):
434 434 """return a set of all obsolescence markers relevant to a set of nodes.
435 435
436 436 "relevant" to a set of nodes mean:
437 437
438 438 - marker that use this changeset as successor
439 439 - prune marker of direct children on this changeset
440 440 - recursive application of the two rules on precursors of these markers
441 441
442 442 It is a set so you cannot rely on order."""
443 443
444 444 pendingnodes = set(nodes)
445 445 seenmarkers = set()
446 446 seennodes = set(pendingnodes)
447 447 precursorsmarkers = self.precursors
448 448 children = self.children
449 449 while pendingnodes:
450 450 direct = set()
451 451 for current in pendingnodes:
452 452 direct.update(precursorsmarkers.get(current, ()))
453 453 pruned = [m for m in children.get(current, ()) if not m[1]]
454 454 direct.update(pruned)
455 455 direct -= seenmarkers
456 456 pendingnodes = set([m[0] for m in direct])
457 457 seenmarkers |= direct
458 458 pendingnodes -= seennodes
459 459 seennodes |= pendingnodes
460 460 return seenmarkers
461 461
462 462 def commonversion(versions):
463 463 """Return the newest version listed in both versions and our local formats.
464 464
465 465 Returns None if no common version exists.
466 466 """
467 467 versions.sort(reverse=True)
468 468 # search for highest version known on both side
469 469 for v in versions:
470 470 if v in formats:
471 471 return v
472 472 return None
473 473
474 474 # arbitrary picked to fit into 8K limit from HTTP server
475 475 # you have to take in account:
476 476 # - the version header
477 477 # - the base85 encoding
478 478 _maxpayload = 5300
479 479
480 480 def _pushkeyescape(markers):
481 481 """encode markers into a dict suitable for pushkey exchange
482 482
483 483 - binary data is base85 encoded
484 484 - split in chunks smaller than 5300 bytes"""
485 485 keys = {}
486 486 parts = []
487 487 currentlen = _maxpayload * 2 # ensure we create a new part
488 488 for marker in markers:
489 489 nextdata = _fm0encodeonemarker(marker)
490 490 if (len(nextdata) + currentlen > _maxpayload):
491 491 currentpart = []
492 492 currentlen = 0
493 493 parts.append(currentpart)
494 494 currentpart.append(nextdata)
495 495 currentlen += len(nextdata)
496 496 for idx, part in enumerate(reversed(parts)):
497 497 data = ''.join([_pack('>B', _fm0version)] + part)
498 498 keys['dump%i' % idx] = base85.b85encode(data)
499 499 return keys
500 500
501 501 def listmarkers(repo):
502 502 """List markers over pushkey"""
503 503 if not repo.obsstore:
504 504 return {}
505 505 return _pushkeyescape(repo.obsstore)
506 506
507 507 def pushmarker(repo, key, old, new):
508 508 """Push markers over pushkey"""
509 509 if not key.startswith('dump'):
510 510 repo.ui.warn(_('unknown key: %r') % key)
511 511 return 0
512 512 if old:
513 513 repo.ui.warn(_('unexpected old value for %r') % key)
514 514 return 0
515 515 data = base85.b85decode(new)
516 516 lock = repo.lock()
517 517 try:
518 518 tr = repo.transaction('pushkey: obsolete markers')
519 519 try:
520 520 repo.obsstore.mergemarkers(tr, data)
521 521 tr.close()
522 522 return 1
523 523 finally:
524 524 tr.release()
525 525 finally:
526 526 lock.release()
527 527
528 528 def getmarkers(repo, nodes=None):
529 529 """returns markers known in a repository
530 530
531 531 If <nodes> is specified, only markers "relevant" to those nodes are are
532 532 returned"""
533 533 if nodes is None:
534 534 rawmarkers = repo.obsstore
535 535 else:
536 536 rawmarkers = repo.obsstore.relevantmarkers(nodes)
537 537
538 538 for markerdata in rawmarkers:
539 539 yield marker(repo, markerdata)
540 540
541 541 def relevantmarkers(repo, node):
542 542 """all obsolete markers relevant to some revision"""
543 543 for markerdata in repo.obsstore.relevantmarkers(node):
544 544 yield marker(repo, markerdata)
545 545
546 546
547 547 def precursormarkers(ctx):
548 548 """obsolete marker marking this changeset as a successors"""
549 549 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
550 550 yield marker(ctx._repo, data)
551 551
552 552 def successormarkers(ctx):
553 553 """obsolete marker making this changeset obsolete"""
554 554 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
555 555 yield marker(ctx._repo, data)
556 556
557 557 def allsuccessors(obsstore, nodes, ignoreflags=0):
558 558 """Yield node for every successor of <nodes>.
559 559
560 560 Some successors may be unknown locally.
561 561
562 562 This is a linear yield unsuited to detecting split changesets. It includes
563 563 initial nodes too."""
564 564 remaining = set(nodes)
565 565 seen = set(remaining)
566 566 while remaining:
567 567 current = remaining.pop()
568 568 yield current
569 569 for mark in obsstore.successors.get(current, ()):
570 570 # ignore marker flagged with specified flag
571 571 if mark[2] & ignoreflags:
572 572 continue
573 573 for suc in mark[1]:
574 574 if suc not in seen:
575 575 seen.add(suc)
576 576 remaining.add(suc)
577 577
578 578 def allprecursors(obsstore, nodes, ignoreflags=0):
579 579 """Yield node for every precursors of <nodes>.
580 580
581 581 Some precursors may be unknown locally.
582 582
583 583 This is a linear yield unsuited to detecting folded changesets. It includes
584 584 initial nodes too."""
585 585
586 586 remaining = set(nodes)
587 587 seen = set(remaining)
588 588 while remaining:
589 589 current = remaining.pop()
590 590 yield current
591 591 for mark in obsstore.precursors.get(current, ()):
592 592 # ignore marker flagged with specified flag
593 593 if mark[2] & ignoreflags:
594 594 continue
595 595 suc = mark[0]
596 596 if suc not in seen:
597 597 seen.add(suc)
598 598 remaining.add(suc)
599 599
600 600 def foreground(repo, nodes):
601 601 """return all nodes in the "foreground" of other node
602 602
603 603 The foreground of a revision is anything reachable using parent -> children
604 604 or precursor -> successor relation. It is very similar to "descendant" but
605 605 augmented with obsolescence information.
606 606
607 607 Beware that possible obsolescence cycle may result if complex situation.
608 608 """
609 609 repo = repo.unfiltered()
610 610 foreground = set(repo.set('%ln::', nodes))
611 611 if repo.obsstore:
612 612 # We only need this complicated logic if there is obsolescence
613 613 # XXX will probably deserve an optimised revset.
614 614 nm = repo.changelog.nodemap
615 615 plen = -1
616 616 # compute the whole set of successors or descendants
617 617 while len(foreground) != plen:
618 618 plen = len(foreground)
619 619 succs = set(c.node() for c in foreground)
620 620 mutable = [c.node() for c in foreground if c.mutable()]
621 621 succs.update(allsuccessors(repo.obsstore, mutable))
622 622 known = (n for n in succs if n in nm)
623 623 foreground = set(repo.set('%ln::', known))
624 624 return set(c.node() for c in foreground)
625 625
626 626
627 627 def successorssets(repo, initialnode, cache=None):
628 628 """Return all set of successors of initial nodes
629 629
630 630 The successors set of a changeset A are a group of revisions that succeed
631 631 A. It succeeds A as a consistent whole, each revision being only a partial
632 632 replacement. The successors set contains non-obsolete changesets only.
633 633
634 634 This function returns the full list of successor sets which is why it
635 635 returns a list of tuples and not just a single tuple. Each tuple is a valid
636 636 successors set. Not that (A,) may be a valid successors set for changeset A
637 637 (see below).
638 638
639 639 In most cases, a changeset A will have a single element (e.g. the changeset
640 640 A is replaced by A') in its successors set. Though, it is also common for a
641 641 changeset A to have no elements in its successor set (e.g. the changeset
642 642 has been pruned). Therefore, the returned list of successors sets will be
643 643 [(A',)] or [], respectively.
644 644
645 645 When a changeset A is split into A' and B', however, it will result in a
646 646 successors set containing more than a single element, i.e. [(A',B')].
647 647 Divergent changesets will result in multiple successors sets, i.e. [(A',),
648 648 (A'')].
649 649
650 650 If a changeset A is not obsolete, then it will conceptually have no
651 651 successors set. To distinguish this from a pruned changeset, the successor
652 652 set will only contain itself, i.e. [(A,)].
653 653
654 654 Finally, successors unknown locally are considered to be pruned (obsoleted
655 655 without any successors).
656 656
657 657 The optional `cache` parameter is a dictionary that may contain precomputed
658 658 successors sets. It is meant to reuse the computation of a previous call to
659 659 `successorssets` when multiple calls are made at the same time. The cache
660 660 dictionary is updated in place. The caller is responsible for its live
661 661 spawn. Code that makes multiple calls to `successorssets` *must* use this
662 662 cache mechanism or suffer terrible performances.
663 663
664 664 """
665 665
666 666 succmarkers = repo.obsstore.successors
667 667
668 668 # Stack of nodes we search successors sets for
669 669 toproceed = [initialnode]
670 670 # set version of above list for fast loop detection
671 671 # element added to "toproceed" must be added here
672 672 stackedset = set(toproceed)
673 673 if cache is None:
674 674 cache = {}
675 675
676 676 # This while loop is the flattened version of a recursive search for
677 677 # successors sets
678 678 #
679 679 # def successorssets(x):
680 680 # successors = directsuccessors(x)
681 681 # ss = [[]]
682 682 # for succ in directsuccessors(x):
683 683 # # product as in itertools cartesian product
684 684 # ss = product(ss, successorssets(succ))
685 685 # return ss
686 686 #
687 687 # But we can not use plain recursive calls here:
688 688 # - that would blow the python call stack
689 689 # - obsolescence markers may have cycles, we need to handle them.
690 690 #
691 691 # The `toproceed` list act as our call stack. Every node we search
692 692 # successors set for are stacked there.
693 693 #
694 694 # The `stackedset` is set version of this stack used to check if a node is
695 695 # already stacked. This check is used to detect cycles and prevent infinite
696 696 # loop.
697 697 #
698 698 # successors set of all nodes are stored in the `cache` dictionary.
699 699 #
700 700 # After this while loop ends we use the cache to return the successors sets
701 701 # for the node requested by the caller.
702 702 while toproceed:
703 703 # Every iteration tries to compute the successors sets of the topmost
704 704 # node of the stack: CURRENT.
705 705 #
706 706 # There are four possible outcomes:
707 707 #
708 708 # 1) We already know the successors sets of CURRENT:
709 709 # -> mission accomplished, pop it from the stack.
710 710 # 2) Node is not obsolete:
711 711 # -> the node is its own successors sets. Add it to the cache.
712 712 # 3) We do not know successors set of direct successors of CURRENT:
713 713 # -> We add those successors to the stack.
714 714 # 4) We know successors sets of all direct successors of CURRENT:
715 715 # -> We can compute CURRENT successors set and add it to the
716 716 # cache.
717 717 #
718 718 current = toproceed[-1]
719 719 if current in cache:
720 720 # case (1): We already know the successors sets
721 721 stackedset.remove(toproceed.pop())
722 722 elif current not in succmarkers:
723 723 # case (2): The node is not obsolete.
724 724 if current in repo:
725 725 # We have a valid last successors.
726 726 cache[current] = [(current,)]
727 727 else:
728 728 # Final obsolete version is unknown locally.
729 729 # Do not count that as a valid successors
730 730 cache[current] = []
731 731 else:
732 732 # cases (3) and (4)
733 733 #
734 734 # We proceed in two phases. Phase 1 aims to distinguish case (3)
735 735 # from case (4):
736 736 #
737 737 # For each direct successors of CURRENT, we check whether its
738 738 # successors sets are known. If they are not, we stack the
739 739 # unknown node and proceed to the next iteration of the while
740 740 # loop. (case 3)
741 741 #
742 742 # During this step, we may detect obsolescence cycles: a node
743 743 # with unknown successors sets but already in the call stack.
744 744 # In such a situation, we arbitrary set the successors sets of
745 745 # the node to nothing (node pruned) to break the cycle.
746 746 #
747 747 # If no break was encountered we proceed to phase 2.
748 748 #
749 749 # Phase 2 computes successors sets of CURRENT (case 4); see details
750 750 # in phase 2 itself.
751 751 #
752 752 # Note the two levels of iteration in each phase.
753 753 # - The first one handles obsolescence markers using CURRENT as
754 754 # precursor (successors markers of CURRENT).
755 755 #
756 756 # Having multiple entry here means divergence.
757 757 #
758 758 # - The second one handles successors defined in each marker.
759 759 #
760 760 # Having none means pruned node, multiple successors means split,
761 761 # single successors are standard replacement.
762 762 #
763 763 for mark in sorted(succmarkers[current]):
764 764 for suc in mark[1]:
765 765 if suc not in cache:
766 766 if suc in stackedset:
767 767 # cycle breaking
768 768 cache[suc] = []
769 769 else:
770 770 # case (3) If we have not computed successors sets
771 771 # of one of those successors we add it to the
772 772 # `toproceed` stack and stop all work for this
773 773 # iteration.
774 774 toproceed.append(suc)
775 775 stackedset.add(suc)
776 776 break
777 777 else:
778 778 continue
779 779 break
780 780 else:
781 781 # case (4): we know all successors sets of all direct
782 782 # successors
783 783 #
784 784 # Successors set contributed by each marker depends on the
785 785 # successors sets of all its "successors" node.
786 786 #
787 787 # Each different marker is a divergence in the obsolescence
788 788 # history. It contributes successors sets distinct from other
789 789 # markers.
790 790 #
791 791 # Within a marker, a successor may have divergent successors
792 792 # sets. In such a case, the marker will contribute multiple
793 793 # divergent successors sets. If multiple successors have
794 794 # divergent successors sets, a Cartesian product is used.
795 795 #
796 796 # At the end we post-process successors sets to remove
797 797 # duplicated entry and successors set that are strict subset of
798 798 # another one.
799 799 succssets = []
800 800 for mark in sorted(succmarkers[current]):
801 801 # successors sets contributed by this marker
802 802 markss = [[]]
803 803 for suc in mark[1]:
804 804 # cardinal product with previous successors
805 805 productresult = []
806 806 for prefix in markss:
807 807 for suffix in cache[suc]:
808 808 newss = list(prefix)
809 809 for part in suffix:
810 810 # do not duplicated entry in successors set
811 811 # first entry wins.
812 812 if part not in newss:
813 813 newss.append(part)
814 814 productresult.append(newss)
815 815 markss = productresult
816 816 succssets.extend(markss)
817 817 # remove duplicated and subset
818 818 seen = []
819 819 final = []
820 820 candidate = sorted(((set(s), s) for s in succssets if s),
821 821 key=lambda x: len(x[1]), reverse=True)
822 822 for setversion, listversion in candidate:
823 823 for seenset in seen:
824 824 if setversion.issubset(seenset):
825 825 break
826 826 else:
827 827 final.append(listversion)
828 828 seen.append(setversion)
829 829 final.reverse() # put small successors set first
830 830 cache[current] = final
831 831 return cache[initialnode]
832 832
833 833 def _knownrevs(repo, nodes):
834 834 """yield revision numbers of known nodes passed in parameters
835 835
836 836 Unknown revisions are silently ignored."""
837 837 torev = repo.changelog.nodemap.get
838 838 for n in nodes:
839 839 rev = torev(n)
840 840 if rev is not None:
841 841 yield rev
842 842
843 843 # mapping of 'set-name' -> <function to compute this set>
844 844 cachefuncs = {}
845 845 def cachefor(name):
846 846 """Decorator to register a function as computing the cache for a set"""
847 847 def decorator(func):
848 848 assert name not in cachefuncs
849 849 cachefuncs[name] = func
850 850 return func
851 851 return decorator
852 852
853 853 def getrevs(repo, name):
854 854 """Return the set of revision that belong to the <name> set
855 855
856 856 Such access may compute the set and cache it for future use"""
857 857 repo = repo.unfiltered()
858 858 if not repo.obsstore:
859 859 return frozenset()
860 860 if name not in repo.obsstore.caches:
861 861 repo.obsstore.caches[name] = cachefuncs[name](repo)
862 862 return repo.obsstore.caches[name]
863 863
864 864 # To be simple we need to invalidate obsolescence cache when:
865 865 #
866 866 # - new changeset is added:
867 867 # - public phase is changed
868 868 # - obsolescence marker are added
869 869 # - strip is used a repo
870 870 def clearobscaches(repo):
871 871 """Remove all obsolescence related cache from a repo
872 872
873 873 This remove all cache in obsstore is the obsstore already exist on the
874 874 repo.
875 875
876 876 (We could be smarter here given the exact event that trigger the cache
877 877 clearing)"""
878 878 # only clear cache is there is obsstore data in this repo
879 879 if 'obsstore' in repo._filecache:
880 880 repo.obsstore.caches.clear()
881 881
882 882 @cachefor('obsolete')
883 883 def _computeobsoleteset(repo):
884 884 """the set of obsolete revisions"""
885 885 obs = set()
886 886 getrev = repo.changelog.nodemap.get
887 887 getphase = repo._phasecache.phase
888 888 for n in repo.obsstore.successors:
889 889 rev = getrev(n)
890 890 if rev is not None and getphase(repo, rev):
891 891 obs.add(rev)
892 892 return obs
893 893
894 894 @cachefor('unstable')
895 895 def _computeunstableset(repo):
896 896 """the set of non obsolete revisions with obsolete parents"""
897 897 # revset is not efficient enough here
898 898 # we do (obsolete()::) - obsolete() by hand
899 899 obs = getrevs(repo, 'obsolete')
900 900 if not obs:
901 901 return set()
902 902 cl = repo.changelog
903 903 return set(r for r in cl.descendants(obs) if r not in obs)
904 904
905 905 @cachefor('suspended')
906 906 def _computesuspendedset(repo):
907 907 """the set of obsolete parents with non obsolete descendants"""
908 908 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
909 909 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
910 910
911 911 @cachefor('extinct')
912 912 def _computeextinctset(repo):
913 913 """the set of obsolete parents without non obsolete descendants"""
914 914 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
915 915
916 916
917 917 @cachefor('bumped')
918 918 def _computebumpedset(repo):
919 919 """the set of revs trying to obsolete public revisions"""
920 920 bumped = set()
921 921 # util function (avoid attribute lookup in the loop)
922 922 phase = repo._phasecache.phase # would be faster to grab the full list
923 923 public = phases.public
924 924 cl = repo.changelog
925 925 torev = cl.nodemap.get
926 926 obs = getrevs(repo, 'obsolete')
927 927 for rev in repo:
928 928 # We only evaluate mutable, non-obsolete revision
929 929 if (public < phase(repo, rev)) and (rev not in obs):
930 930 node = cl.node(rev)
931 931 # (future) A cache of precursors may worth if split is very common
932 932 for pnode in allprecursors(repo.obsstore, [node],
933 933 ignoreflags=bumpedfix):
934 934 prev = torev(pnode) # unfiltered! but so is phasecache
935 935 if (prev is not None) and (phase(repo, prev) <= public):
936 936 # we have a public precursors
937 937 bumped.add(rev)
938 938 break # Next draft!
939 939 return bumped
940 940
941 941 @cachefor('divergent')
942 942 def _computedivergentset(repo):
943 943 """the set of rev that compete to be the final successors of some revision.
944 944 """
945 945 divergent = set()
946 946 obsstore = repo.obsstore
947 947 newermap = {}
948 948 for ctx in repo.set('(not public()) - obsolete()'):
949 949 mark = obsstore.precursors.get(ctx.node(), ())
950 950 toprocess = set(mark)
951 951 while toprocess:
952 952 prec = toprocess.pop()[0]
953 953 if prec not in newermap:
954 954 successorssets(repo, prec, newermap)
955 955 newer = [n for n in newermap[prec] if n]
956 956 if len(newer) > 1:
957 957 divergent.add(ctx.rev())
958 958 break
959 959 toprocess.update(obsstore.precursors.get(prec, ()))
960 960 return divergent
961 961
962 962
963 963 def createmarkers(repo, relations, flag=0, date=None, metadata=None):
964 964 """Add obsolete markers between changesets in a repo
965 965
966 966 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
967 967 tuple. `old` and `news` are changectx. metadata is an optional dictionary
968 968 containing metadata for this marker only. It is merged with the global
969 969 metadata specified through the `metadata` argument of this function,
970 970
971 971 Trying to obsolete a public changeset will raise an exception.
972 972
973 973 Current user and date are used except if specified otherwise in the
974 974 metadata attribute.
975 975
976 976 This function operates within a transaction of its own, but does
977 977 not take any lock on the repo.
978 978 """
979 979 # prepare metadata
980 980 if metadata is None:
981 981 metadata = {}
982 982 if 'user' not in metadata:
983 983 metadata['user'] = repo.ui.username()
984 984 tr = repo.transaction('add-obsolescence-marker')
985 985 try:
986 986 for rel in relations:
987 987 prec = rel[0]
988 988 sucs = rel[1]
989 989 localmetadata = metadata.copy()
990 990 if 2 < len(rel):
991 991 localmetadata.update(rel[2])
992 992
993 993 if not prec.mutable():
994 994 raise util.Abort("cannot obsolete immutable changeset: %s"
995 995 % prec)
996 996 nprec = prec.node()
997 997 nsucs = tuple(s.node() for s in sucs)
998 998 npare = None
999 999 if not nsucs:
1000 1000 npare = tuple(p.node() for p in prec.parents())
1001 1001 if nprec in nsucs:
1002 1002 raise util.Abort("changeset %s cannot obsolete itself" % prec)
1003 1003 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1004 1004 date=date, metadata=localmetadata)
1005 1005 repo.filteredrevcache.clear()
1006 1006 tr.close()
1007 1007 finally:
1008 1008 tr.release()
General Comments 0
You need to be logged in to leave comments. Login now