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