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