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