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