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