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