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