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