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