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