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