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