##// END OF EJS Templates
obsolete: don't import from .node twice...
av6 -
r49544:48cb4109 default
parent child Browse files
Show More
@@ -1,1150 +1,1146 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 "predecessor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a predecessor 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 "predecessor markers of Y" because they hold
28 28 information about the predecessors 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 is used:
46 46
47 47 (A, (B, 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 from __future__ import absolute_import
71 71
72 72 import errno
73 73 import struct
74 74
75 75 from .i18n import _
76 from .node import (
77 bin,
78 hex,
79 )
80 76 from .pycompat import getattr
81 77 from .node import (
82 78 bin,
83 79 hex,
84 80 )
85 81 from . import (
86 82 encoding,
87 83 error,
88 84 obsutil,
89 85 phases,
90 86 policy,
91 87 pycompat,
92 88 util,
93 89 )
94 90 from .utils import (
95 91 dateutil,
96 92 hashutil,
97 93 )
98 94
99 95 parsers = policy.importmod('parsers')
100 96
101 97 _pack = struct.pack
102 98 _unpack = struct.unpack
103 99 _calcsize = struct.calcsize
104 100 propertycache = util.propertycache
105 101
106 102 # Options for obsolescence
107 103 createmarkersopt = b'createmarkers'
108 104 allowunstableopt = b'allowunstable'
109 105 allowdivergenceopt = b'allowdivergence'
110 106 exchangeopt = b'exchange'
111 107
112 108
113 109 def _getoptionvalue(repo, option):
114 110 """Returns True if the given repository has the given obsolete option
115 111 enabled.
116 112 """
117 113 configkey = b'evolution.%s' % option
118 114 newconfig = repo.ui.configbool(b'experimental', configkey)
119 115
120 116 # Return the value only if defined
121 117 if newconfig is not None:
122 118 return newconfig
123 119
124 120 # Fallback on generic option
125 121 try:
126 122 return repo.ui.configbool(b'experimental', b'evolution')
127 123 except (error.ConfigError, AttributeError):
128 124 # Fallback on old-fashion config
129 125 # inconsistent config: experimental.evolution
130 126 result = set(repo.ui.configlist(b'experimental', b'evolution'))
131 127
132 128 if b'all' in result:
133 129 return True
134 130
135 131 # Temporary hack for next check
136 132 newconfig = repo.ui.config(b'experimental', b'evolution.createmarkers')
137 133 if newconfig:
138 134 result.add(b'createmarkers')
139 135
140 136 return option in result
141 137
142 138
143 139 def getoptions(repo):
144 140 """Returns dicts showing state of obsolescence features."""
145 141
146 142 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
147 143 if createmarkersvalue:
148 144 unstablevalue = _getoptionvalue(repo, allowunstableopt)
149 145 divergencevalue = _getoptionvalue(repo, allowdivergenceopt)
150 146 exchangevalue = _getoptionvalue(repo, exchangeopt)
151 147 else:
152 148 # if we cannot create obsolescence markers, we shouldn't exchange them
153 149 # or perform operations that lead to instability or divergence
154 150 unstablevalue = False
155 151 divergencevalue = False
156 152 exchangevalue = False
157 153
158 154 return {
159 155 createmarkersopt: createmarkersvalue,
160 156 allowunstableopt: unstablevalue,
161 157 allowdivergenceopt: divergencevalue,
162 158 exchangeopt: exchangevalue,
163 159 }
164 160
165 161
166 162 def isenabled(repo, option):
167 163 """Returns True if the given repository has the given obsolete option
168 164 enabled.
169 165 """
170 166 return getoptions(repo)[option]
171 167
172 168
173 169 # Creating aliases for marker flags because evolve extension looks for
174 170 # bumpedfix in obsolete.py
175 171 bumpedfix = obsutil.bumpedfix
176 172 usingsha256 = obsutil.usingsha256
177 173
178 174 ## Parsing and writing of version "0"
179 175 #
180 176 # The header is followed by the markers. Each marker is made of:
181 177 #
182 178 # - 1 uint8 : number of new changesets "N", can be zero.
183 179 #
184 180 # - 1 uint32: metadata size "M" in bytes.
185 181 #
186 182 # - 1 byte: a bit field. It is reserved for flags used in common
187 183 # obsolete marker operations, to avoid repeated decoding of metadata
188 184 # entries.
189 185 #
190 186 # - 20 bytes: obsoleted changeset identifier.
191 187 #
192 188 # - N*20 bytes: new changesets identifiers.
193 189 #
194 190 # - M bytes: metadata as a sequence of nul-terminated strings. Each
195 191 # string contains a key and a value, separated by a colon ':', without
196 192 # additional encoding. Keys cannot contain '\0' or ':' and values
197 193 # cannot contain '\0'.
198 194 _fm0version = 0
199 195 _fm0fixed = b'>BIB20s'
200 196 _fm0node = b'20s'
201 197 _fm0fsize = _calcsize(_fm0fixed)
202 198 _fm0fnodesize = _calcsize(_fm0node)
203 199
204 200
205 201 def _fm0readmarkers(data, off, stop):
206 202 # Loop on markers
207 203 while off < stop:
208 204 # read fixed part
209 205 cur = data[off : off + _fm0fsize]
210 206 off += _fm0fsize
211 207 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
212 208 # read replacement
213 209 sucs = ()
214 210 if numsuc:
215 211 s = _fm0fnodesize * numsuc
216 212 cur = data[off : off + s]
217 213 sucs = _unpack(_fm0node * numsuc, cur)
218 214 off += s
219 215 # read metadata
220 216 # (metadata will be decoded on demand)
221 217 metadata = data[off : off + mdsize]
222 218 if len(metadata) != mdsize:
223 219 raise error.Abort(
224 220 _(
225 221 b'parsing obsolete marker: metadata is too '
226 222 b'short, %d bytes expected, got %d'
227 223 )
228 224 % (mdsize, len(metadata))
229 225 )
230 226 off += mdsize
231 227 metadata = _fm0decodemeta(metadata)
232 228 try:
233 229 when, offset = metadata.pop(b'date', b'0 0').split(b' ')
234 230 date = float(when), int(offset)
235 231 except ValueError:
236 232 date = (0.0, 0)
237 233 parents = None
238 234 if b'p2' in metadata:
239 235 parents = (metadata.pop(b'p1', None), metadata.pop(b'p2', None))
240 236 elif b'p1' in metadata:
241 237 parents = (metadata.pop(b'p1', None),)
242 238 elif b'p0' in metadata:
243 239 parents = ()
244 240 if parents is not None:
245 241 try:
246 242 parents = tuple(bin(p) for p in parents)
247 243 # if parent content is not a nodeid, drop the data
248 244 for p in parents:
249 245 if len(p) != 20:
250 246 parents = None
251 247 break
252 248 except TypeError:
253 249 # if content cannot be translated to nodeid drop the data.
254 250 parents = None
255 251
256 252 metadata = tuple(sorted(pycompat.iteritems(metadata)))
257 253
258 254 yield (pre, sucs, flags, metadata, date, parents)
259 255
260 256
261 257 def _fm0encodeonemarker(marker):
262 258 pre, sucs, flags, metadata, date, parents = marker
263 259 if flags & usingsha256:
264 260 raise error.Abort(_(b'cannot handle sha256 with old obsstore format'))
265 261 metadata = dict(metadata)
266 262 time, tz = date
267 263 metadata[b'date'] = b'%r %i' % (time, tz)
268 264 if parents is not None:
269 265 if not parents:
270 266 # mark that we explicitly recorded no parents
271 267 metadata[b'p0'] = b''
272 268 for i, p in enumerate(parents, 1):
273 269 metadata[b'p%i' % i] = hex(p)
274 270 metadata = _fm0encodemeta(metadata)
275 271 numsuc = len(sucs)
276 272 format = _fm0fixed + (_fm0node * numsuc)
277 273 data = [numsuc, len(metadata), flags, pre]
278 274 data.extend(sucs)
279 275 return _pack(format, *data) + metadata
280 276
281 277
282 278 def _fm0encodemeta(meta):
283 279 """Return encoded metadata string to string mapping.
284 280
285 281 Assume no ':' in key and no '\0' in both key and value."""
286 282 for key, value in pycompat.iteritems(meta):
287 283 if b':' in key or b'\0' in key:
288 284 raise ValueError(b"':' and '\0' are forbidden in metadata key'")
289 285 if b'\0' in value:
290 286 raise ValueError(b"':' is forbidden in metadata value'")
291 287 return b'\0'.join([b'%s:%s' % (k, meta[k]) for k in sorted(meta)])
292 288
293 289
294 290 def _fm0decodemeta(data):
295 291 """Return string to string dictionary from encoded version."""
296 292 d = {}
297 293 for l in data.split(b'\0'):
298 294 if l:
299 295 key, value = l.split(b':', 1)
300 296 d[key] = value
301 297 return d
302 298
303 299
304 300 ## Parsing and writing of version "1"
305 301 #
306 302 # The header is followed by the markers. Each marker is made of:
307 303 #
308 304 # - uint32: total size of the marker (including this field)
309 305 #
310 306 # - float64: date in seconds since epoch
311 307 #
312 308 # - int16: timezone offset in minutes
313 309 #
314 310 # - uint16: a bit field. It is reserved for flags used in common
315 311 # obsolete marker operations, to avoid repeated decoding of metadata
316 312 # entries.
317 313 #
318 314 # - uint8: number of successors "N", can be zero.
319 315 #
320 316 # - uint8: number of parents "P", can be zero.
321 317 #
322 318 # 0: parents data stored but no parent,
323 319 # 1: one parent stored,
324 320 # 2: two parents stored,
325 321 # 3: no parent data stored
326 322 #
327 323 # - uint8: number of metadata entries M
328 324 #
329 325 # - 20 or 32 bytes: predecessor changeset identifier.
330 326 #
331 327 # - N*(20 or 32) bytes: successors changesets identifiers.
332 328 #
333 329 # - P*(20 or 32) bytes: parents of the predecessors changesets.
334 330 #
335 331 # - M*(uint8, uint8): size of all metadata entries (key and value)
336 332 #
337 333 # - remaining bytes: the metadata, each (key, value) pair after the other.
338 334 _fm1version = 1
339 335 _fm1fixed = b'>IdhHBBB'
340 336 _fm1nodesha1 = b'20s'
341 337 _fm1nodesha256 = b'32s'
342 338 _fm1nodesha1size = _calcsize(_fm1nodesha1)
343 339 _fm1nodesha256size = _calcsize(_fm1nodesha256)
344 340 _fm1fsize = _calcsize(_fm1fixed)
345 341 _fm1parentnone = 3
346 342 _fm1parentshift = 14
347 343 _fm1parentmask = _fm1parentnone << _fm1parentshift
348 344 _fm1metapair = b'BB'
349 345 _fm1metapairsize = _calcsize(_fm1metapair)
350 346
351 347
352 348 def _fm1purereadmarkers(data, off, stop):
353 349 # make some global constants local for performance
354 350 noneflag = _fm1parentnone
355 351 sha2flag = usingsha256
356 352 sha1size = _fm1nodesha1size
357 353 sha2size = _fm1nodesha256size
358 354 sha1fmt = _fm1nodesha1
359 355 sha2fmt = _fm1nodesha256
360 356 metasize = _fm1metapairsize
361 357 metafmt = _fm1metapair
362 358 fsize = _fm1fsize
363 359 unpack = _unpack
364 360
365 361 # Loop on markers
366 362 ufixed = struct.Struct(_fm1fixed).unpack
367 363
368 364 while off < stop:
369 365 # read fixed part
370 366 o1 = off + fsize
371 367 t, secs, tz, flags, numsuc, numpar, nummeta = ufixed(data[off:o1])
372 368
373 369 if flags & sha2flag:
374 370 nodefmt = sha2fmt
375 371 nodesize = sha2size
376 372 else:
377 373 nodefmt = sha1fmt
378 374 nodesize = sha1size
379 375
380 376 (prec,) = unpack(nodefmt, data[o1 : o1 + nodesize])
381 377 o1 += nodesize
382 378
383 379 # read 0 or more successors
384 380 if numsuc == 1:
385 381 o2 = o1 + nodesize
386 382 sucs = (data[o1:o2],)
387 383 else:
388 384 o2 = o1 + nodesize * numsuc
389 385 sucs = unpack(nodefmt * numsuc, data[o1:o2])
390 386
391 387 # read parents
392 388 if numpar == noneflag:
393 389 o3 = o2
394 390 parents = None
395 391 elif numpar == 1:
396 392 o3 = o2 + nodesize
397 393 parents = (data[o2:o3],)
398 394 else:
399 395 o3 = o2 + nodesize * numpar
400 396 parents = unpack(nodefmt * numpar, data[o2:o3])
401 397
402 398 # read metadata
403 399 off = o3 + metasize * nummeta
404 400 metapairsize = unpack(b'>' + (metafmt * nummeta), data[o3:off])
405 401 metadata = []
406 402 for idx in pycompat.xrange(0, len(metapairsize), 2):
407 403 o1 = off + metapairsize[idx]
408 404 o2 = o1 + metapairsize[idx + 1]
409 405 metadata.append((data[off:o1], data[o1:o2]))
410 406 off = o2
411 407
412 408 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
413 409
414 410
415 411 def _fm1encodeonemarker(marker):
416 412 pre, sucs, flags, metadata, date, parents = marker
417 413 # determine node size
418 414 _fm1node = _fm1nodesha1
419 415 if flags & usingsha256:
420 416 _fm1node = _fm1nodesha256
421 417 numsuc = len(sucs)
422 418 numextranodes = 1 + numsuc
423 419 if parents is None:
424 420 numpar = _fm1parentnone
425 421 else:
426 422 numpar = len(parents)
427 423 numextranodes += numpar
428 424 formatnodes = _fm1node * numextranodes
429 425 formatmeta = _fm1metapair * len(metadata)
430 426 format = _fm1fixed + formatnodes + formatmeta
431 427 # tz is stored in minutes so we divide by 60
432 428 tz = date[1] // 60
433 429 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
434 430 data.extend(sucs)
435 431 if parents is not None:
436 432 data.extend(parents)
437 433 totalsize = _calcsize(format)
438 434 for key, value in metadata:
439 435 lk = len(key)
440 436 lv = len(value)
441 437 if lk > 255:
442 438 msg = (
443 439 b'obsstore metadata key cannot be longer than 255 bytes'
444 440 b' (key "%s" is %u bytes)'
445 441 ) % (key, lk)
446 442 raise error.ProgrammingError(msg)
447 443 if lv > 255:
448 444 msg = (
449 445 b'obsstore metadata value cannot be longer than 255 bytes'
450 446 b' (value "%s" for key "%s" is %u bytes)'
451 447 ) % (value, key, lv)
452 448 raise error.ProgrammingError(msg)
453 449 data.append(lk)
454 450 data.append(lv)
455 451 totalsize += lk + lv
456 452 data[0] = totalsize
457 453 data = [_pack(format, *data)]
458 454 for key, value in metadata:
459 455 data.append(key)
460 456 data.append(value)
461 457 return b''.join(data)
462 458
463 459
464 460 def _fm1readmarkers(data, off, stop):
465 461 native = getattr(parsers, 'fm1readmarkers', None)
466 462 if not native:
467 463 return _fm1purereadmarkers(data, off, stop)
468 464 return native(data, off, stop)
469 465
470 466
471 467 # mapping to read/write various marker formats
472 468 # <version> -> (decoder, encoder)
473 469 formats = {
474 470 _fm0version: (_fm0readmarkers, _fm0encodeonemarker),
475 471 _fm1version: (_fm1readmarkers, _fm1encodeonemarker),
476 472 }
477 473
478 474
479 475 def _readmarkerversion(data):
480 476 return _unpack(b'>B', data[0:1])[0]
481 477
482 478
483 479 @util.nogc
484 480 def _readmarkers(data, off=None, stop=None):
485 481 """Read and enumerate markers from raw data"""
486 482 diskversion = _readmarkerversion(data)
487 483 if not off:
488 484 off = 1 # skip 1 byte version number
489 485 if stop is None:
490 486 stop = len(data)
491 487 if diskversion not in formats:
492 488 msg = _(b'parsing obsolete marker: unknown version %r') % diskversion
493 489 raise error.UnknownVersion(msg, version=diskversion)
494 490 return diskversion, formats[diskversion][0](data, off, stop)
495 491
496 492
497 493 def encodeheader(version=_fm0version):
498 494 return _pack(b'>B', version)
499 495
500 496
501 497 def encodemarkers(markers, addheader=False, version=_fm0version):
502 498 # Kept separate from flushmarkers(), it will be reused for
503 499 # markers exchange.
504 500 encodeone = formats[version][1]
505 501 if addheader:
506 502 yield encodeheader(version)
507 503 for marker in markers:
508 504 yield encodeone(marker)
509 505
510 506
511 507 @util.nogc
512 508 def _addsuccessors(successors, markers):
513 509 for mark in markers:
514 510 successors.setdefault(mark[0], set()).add(mark)
515 511
516 512
517 513 @util.nogc
518 514 def _addpredecessors(predecessors, markers):
519 515 for mark in markers:
520 516 for suc in mark[1]:
521 517 predecessors.setdefault(suc, set()).add(mark)
522 518
523 519
524 520 @util.nogc
525 521 def _addchildren(children, markers):
526 522 for mark in markers:
527 523 parents = mark[5]
528 524 if parents is not None:
529 525 for p in parents:
530 526 children.setdefault(p, set()).add(mark)
531 527
532 528
533 529 def _checkinvalidmarkers(repo, markers):
534 530 """search for marker with invalid data and raise error if needed
535 531
536 532 Exist as a separated function to allow the evolve extension for a more
537 533 subtle handling.
538 534 """
539 535 for mark in markers:
540 536 if repo.nullid in mark[1]:
541 537 raise error.Abort(
542 538 _(
543 539 b'bad obsolescence marker detected: '
544 540 b'invalid successors nullid'
545 541 )
546 542 )
547 543
548 544
549 545 class obsstore(object):
550 546 """Store obsolete markers
551 547
552 548 Markers can be accessed with two mappings:
553 549 - predecessors[x] -> set(markers on predecessors edges of x)
554 550 - successors[x] -> set(markers on successors edges of x)
555 551 - children[x] -> set(markers on predecessors edges of children(x)
556 552 """
557 553
558 554 fields = (b'prec', b'succs', b'flag', b'meta', b'date', b'parents')
559 555 # prec: nodeid, predecessors changesets
560 556 # succs: tuple of nodeid, successor changesets (0-N length)
561 557 # flag: integer, flag field carrying modifier for the markers (see doc)
562 558 # meta: binary blob in UTF-8, encoded metadata dictionary
563 559 # date: (float, int) tuple, date of marker creation
564 560 # parents: (tuple of nodeid) or None, parents of predecessors
565 561 # None is used when no data has been recorded
566 562
567 563 def __init__(self, repo, svfs, defaultformat=_fm1version, readonly=False):
568 564 # caches for various obsolescence related cache
569 565 self.caches = {}
570 566 self.svfs = svfs
571 567 self.repo = repo
572 568 self._defaultformat = defaultformat
573 569 self._readonly = readonly
574 570
575 571 def __iter__(self):
576 572 return iter(self._all)
577 573
578 574 def __len__(self):
579 575 return len(self._all)
580 576
581 577 def __nonzero__(self):
582 578 if not self._cached('_all'):
583 579 try:
584 580 return self.svfs.stat(b'obsstore').st_size > 1
585 581 except OSError as inst:
586 582 if inst.errno not in (errno.ENOENT, errno.EINVAL):
587 583 raise
588 584 # just build an empty _all list if no obsstore exists, which
589 585 # avoids further stat() syscalls
590 586 return bool(self._all)
591 587
592 588 __bool__ = __nonzero__
593 589
594 590 @property
595 591 def readonly(self):
596 592 """True if marker creation is disabled
597 593
598 594 Remove me in the future when obsolete marker is always on."""
599 595 return self._readonly
600 596
601 597 def create(
602 598 self,
603 599 transaction,
604 600 prec,
605 601 succs=(),
606 602 flag=0,
607 603 parents=None,
608 604 date=None,
609 605 metadata=None,
610 606 ui=None,
611 607 ):
612 608 """obsolete: add a new obsolete marker
613 609
614 610 * ensuring it is hashable
615 611 * check mandatory metadata
616 612 * encode metadata
617 613
618 614 If you are a human writing code creating marker you want to use the
619 615 `createmarkers` function in this module instead.
620 616
621 617 return True if a new marker have been added, False if the markers
622 618 already existed (no op).
623 619 """
624 620 flag = int(flag)
625 621 if metadata is None:
626 622 metadata = {}
627 623 if date is None:
628 624 if b'date' in metadata:
629 625 # as a courtesy for out-of-tree extensions
630 626 date = dateutil.parsedate(metadata.pop(b'date'))
631 627 elif ui is not None:
632 628 date = ui.configdate(b'devel', b'default-date')
633 629 if date is None:
634 630 date = dateutil.makedate()
635 631 else:
636 632 date = dateutil.makedate()
637 633 if flag & usingsha256:
638 634 if len(prec) != 32:
639 635 raise ValueError(prec)
640 636 for succ in succs:
641 637 if len(succ) != 32:
642 638 raise ValueError(succ)
643 639 else:
644 640 if len(prec) != 20:
645 641 raise ValueError(prec)
646 642 for succ in succs:
647 643 if len(succ) != 20:
648 644 raise ValueError(succ)
649 645 if prec in succs:
650 646 raise ValueError(
651 647 'in-marker cycle with %s' % pycompat.sysstr(hex(prec))
652 648 )
653 649
654 650 metadata = tuple(sorted(pycompat.iteritems(metadata)))
655 651 for k, v in metadata:
656 652 try:
657 653 # might be better to reject non-ASCII keys
658 654 k.decode('utf-8')
659 655 v.decode('utf-8')
660 656 except UnicodeDecodeError:
661 657 raise error.ProgrammingError(
662 658 b'obsstore metadata must be valid UTF-8 sequence '
663 659 b'(key = %r, value = %r)'
664 660 % (pycompat.bytestr(k), pycompat.bytestr(v))
665 661 )
666 662
667 663 marker = (bytes(prec), tuple(succs), flag, metadata, date, parents)
668 664 return bool(self.add(transaction, [marker]))
669 665
670 666 def add(self, transaction, markers):
671 667 """Add new markers to the store
672 668
673 669 Take care of filtering duplicate.
674 670 Return the number of new marker."""
675 671 if self._readonly:
676 672 raise error.Abort(
677 673 _(b'creating obsolete markers is not enabled on this repo')
678 674 )
679 675 known = set()
680 676 getsuccessors = self.successors.get
681 677 new = []
682 678 for m in markers:
683 679 if m not in getsuccessors(m[0], ()) and m not in known:
684 680 known.add(m)
685 681 new.append(m)
686 682 if new:
687 683 f = self.svfs(b'obsstore', b'ab')
688 684 try:
689 685 offset = f.tell()
690 686 transaction.add(b'obsstore', offset)
691 687 # offset == 0: new file - add the version header
692 688 data = b''.join(encodemarkers(new, offset == 0, self._version))
693 689 f.write(data)
694 690 finally:
695 691 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
696 692 # call 'filecacheentry.refresh()' here
697 693 f.close()
698 694 addedmarkers = transaction.changes.get(b'obsmarkers')
699 695 if addedmarkers is not None:
700 696 addedmarkers.update(new)
701 697 self._addmarkers(new, data)
702 698 # new marker *may* have changed several set. invalidate the cache.
703 699 self.caches.clear()
704 700 # records the number of new markers for the transaction hooks
705 701 previous = int(transaction.hookargs.get(b'new_obsmarkers', b'0'))
706 702 transaction.hookargs[b'new_obsmarkers'] = b'%d' % (previous + len(new))
707 703 return len(new)
708 704
709 705 def mergemarkers(self, transaction, data):
710 706 """merge a binary stream of markers inside the obsstore
711 707
712 708 Returns the number of new markers added."""
713 709 version, markers = _readmarkers(data)
714 710 return self.add(transaction, markers)
715 711
716 712 @propertycache
717 713 def _data(self):
718 714 return self.svfs.tryread(b'obsstore')
719 715
720 716 @propertycache
721 717 def _version(self):
722 718 if len(self._data) >= 1:
723 719 return _readmarkerversion(self._data)
724 720 else:
725 721 return self._defaultformat
726 722
727 723 @propertycache
728 724 def _all(self):
729 725 data = self._data
730 726 if not data:
731 727 return []
732 728 self._version, markers = _readmarkers(data)
733 729 markers = list(markers)
734 730 _checkinvalidmarkers(self.repo, markers)
735 731 return markers
736 732
737 733 @propertycache
738 734 def successors(self):
739 735 successors = {}
740 736 _addsuccessors(successors, self._all)
741 737 return successors
742 738
743 739 @propertycache
744 740 def predecessors(self):
745 741 predecessors = {}
746 742 _addpredecessors(predecessors, self._all)
747 743 return predecessors
748 744
749 745 @propertycache
750 746 def children(self):
751 747 children = {}
752 748 _addchildren(children, self._all)
753 749 return children
754 750
755 751 def _cached(self, attr):
756 752 return attr in self.__dict__
757 753
758 754 def _addmarkers(self, markers, rawdata):
759 755 markers = list(markers) # to allow repeated iteration
760 756 self._data = self._data + rawdata
761 757 self._all.extend(markers)
762 758 if self._cached('successors'):
763 759 _addsuccessors(self.successors, markers)
764 760 if self._cached('predecessors'):
765 761 _addpredecessors(self.predecessors, markers)
766 762 if self._cached('children'):
767 763 _addchildren(self.children, markers)
768 764 _checkinvalidmarkers(self.repo, markers)
769 765
770 766 def relevantmarkers(self, nodes):
771 767 """return a set of all obsolescence markers relevant to a set of nodes.
772 768
773 769 "relevant" to a set of nodes mean:
774 770
775 771 - marker that use this changeset as successor
776 772 - prune marker of direct children on this changeset
777 773 - recursive application of the two rules on predecessors of these
778 774 markers
779 775
780 776 It is a set so you cannot rely on order."""
781 777
782 778 pendingnodes = set(nodes)
783 779 seenmarkers = set()
784 780 seennodes = set(pendingnodes)
785 781 precursorsmarkers = self.predecessors
786 782 succsmarkers = self.successors
787 783 children = self.children
788 784 while pendingnodes:
789 785 direct = set()
790 786 for current in pendingnodes:
791 787 direct.update(precursorsmarkers.get(current, ()))
792 788 pruned = [m for m in children.get(current, ()) if not m[1]]
793 789 direct.update(pruned)
794 790 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
795 791 direct.update(pruned)
796 792 direct -= seenmarkers
797 793 pendingnodes = {m[0] for m in direct}
798 794 seenmarkers |= direct
799 795 pendingnodes -= seennodes
800 796 seennodes |= pendingnodes
801 797 return seenmarkers
802 798
803 799
804 800 def makestore(ui, repo):
805 801 """Create an obsstore instance from a repo."""
806 802 # read default format for new obsstore.
807 803 # developer config: format.obsstore-version
808 804 defaultformat = ui.configint(b'format', b'obsstore-version')
809 805 # rely on obsstore class default when possible.
810 806 kwargs = {}
811 807 if defaultformat is not None:
812 808 kwargs['defaultformat'] = defaultformat
813 809 readonly = not isenabled(repo, createmarkersopt)
814 810 store = obsstore(repo, repo.svfs, readonly=readonly, **kwargs)
815 811 if store and readonly:
816 812 ui.warn(
817 813 _(b'obsolete feature not enabled but %i markers found!\n')
818 814 % len(list(store))
819 815 )
820 816 return store
821 817
822 818
823 819 def commonversion(versions):
824 820 """Return the newest version listed in both versions and our local formats.
825 821
826 822 Returns None if no common version exists.
827 823 """
828 824 versions.sort(reverse=True)
829 825 # search for highest version known on both side
830 826 for v in versions:
831 827 if v in formats:
832 828 return v
833 829 return None
834 830
835 831
836 832 # arbitrary picked to fit into 8K limit from HTTP server
837 833 # you have to take in account:
838 834 # - the version header
839 835 # - the base85 encoding
840 836 _maxpayload = 5300
841 837
842 838
843 839 def _pushkeyescape(markers):
844 840 """encode markers into a dict suitable for pushkey exchange
845 841
846 842 - binary data is base85 encoded
847 843 - split in chunks smaller than 5300 bytes"""
848 844 keys = {}
849 845 parts = []
850 846 currentlen = _maxpayload * 2 # ensure we create a new part
851 847 for marker in markers:
852 848 nextdata = _fm0encodeonemarker(marker)
853 849 if len(nextdata) + currentlen > _maxpayload:
854 850 currentpart = []
855 851 currentlen = 0
856 852 parts.append(currentpart)
857 853 currentpart.append(nextdata)
858 854 currentlen += len(nextdata)
859 855 for idx, part in enumerate(reversed(parts)):
860 856 data = b''.join([_pack(b'>B', _fm0version)] + part)
861 857 keys[b'dump%i' % idx] = util.b85encode(data)
862 858 return keys
863 859
864 860
865 861 def listmarkers(repo):
866 862 """List markers over pushkey"""
867 863 if not repo.obsstore:
868 864 return {}
869 865 return _pushkeyescape(sorted(repo.obsstore))
870 866
871 867
872 868 def pushmarker(repo, key, old, new):
873 869 """Push markers over pushkey"""
874 870 if not key.startswith(b'dump'):
875 871 repo.ui.warn(_(b'unknown key: %r') % key)
876 872 return False
877 873 if old:
878 874 repo.ui.warn(_(b'unexpected old value for %r') % key)
879 875 return False
880 876 data = util.b85decode(new)
881 877 with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr:
882 878 repo.obsstore.mergemarkers(tr, data)
883 879 repo.invalidatevolatilesets()
884 880 return True
885 881
886 882
887 883 # mapping of 'set-name' -> <function to compute this set>
888 884 cachefuncs = {}
889 885
890 886
891 887 def cachefor(name):
892 888 """Decorator to register a function as computing the cache for a set"""
893 889
894 890 def decorator(func):
895 891 if name in cachefuncs:
896 892 msg = b"duplicated registration for volatileset '%s' (existing: %r)"
897 893 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
898 894 cachefuncs[name] = func
899 895 return func
900 896
901 897 return decorator
902 898
903 899
904 900 def getrevs(repo, name):
905 901 """Return the set of revision that belong to the <name> set
906 902
907 903 Such access may compute the set and cache it for future use"""
908 904 repo = repo.unfiltered()
909 905 with util.timedcm('getrevs %s', name):
910 906 if not repo.obsstore:
911 907 return frozenset()
912 908 if name not in repo.obsstore.caches:
913 909 repo.obsstore.caches[name] = cachefuncs[name](repo)
914 910 return repo.obsstore.caches[name]
915 911
916 912
917 913 # To be simple we need to invalidate obsolescence cache when:
918 914 #
919 915 # - new changeset is added:
920 916 # - public phase is changed
921 917 # - obsolescence marker are added
922 918 # - strip is used a repo
923 919 def clearobscaches(repo):
924 920 """Remove all obsolescence related cache from a repo
925 921
926 922 This remove all cache in obsstore is the obsstore already exist on the
927 923 repo.
928 924
929 925 (We could be smarter here given the exact event that trigger the cache
930 926 clearing)"""
931 927 # only clear cache is there is obsstore data in this repo
932 928 if b'obsstore' in repo._filecache:
933 929 repo.obsstore.caches.clear()
934 930
935 931
936 932 def _mutablerevs(repo):
937 933 """the set of mutable revision in the repository"""
938 934 return repo._phasecache.getrevset(repo, phases.mutablephases)
939 935
940 936
941 937 @cachefor(b'obsolete')
942 938 def _computeobsoleteset(repo):
943 939 """the set of obsolete revisions"""
944 940 getnode = repo.changelog.node
945 941 notpublic = _mutablerevs(repo)
946 942 isobs = repo.obsstore.successors.__contains__
947 943 obs = {r for r in notpublic if isobs(getnode(r))}
948 944 return obs
949 945
950 946
951 947 @cachefor(b'orphan')
952 948 def _computeorphanset(repo):
953 949 """the set of non obsolete revisions with obsolete parents"""
954 950 pfunc = repo.changelog.parentrevs
955 951 mutable = _mutablerevs(repo)
956 952 obsolete = getrevs(repo, b'obsolete')
957 953 others = mutable - obsolete
958 954 unstable = set()
959 955 for r in sorted(others):
960 956 # A rev is unstable if one of its parent is obsolete or unstable
961 957 # this works since we traverse following growing rev order
962 958 for p in pfunc(r):
963 959 if p in obsolete or p in unstable:
964 960 unstable.add(r)
965 961 break
966 962 return unstable
967 963
968 964
969 965 @cachefor(b'suspended')
970 966 def _computesuspendedset(repo):
971 967 """the set of obsolete parents with non obsolete descendants"""
972 968 suspended = repo.changelog.ancestors(getrevs(repo, b'orphan'))
973 969 return {r for r in getrevs(repo, b'obsolete') if r in suspended}
974 970
975 971
976 972 @cachefor(b'extinct')
977 973 def _computeextinctset(repo):
978 974 """the set of obsolete parents without non obsolete descendants"""
979 975 return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended')
980 976
981 977
982 978 @cachefor(b'phasedivergent')
983 979 def _computephasedivergentset(repo):
984 980 """the set of revs trying to obsolete public revisions"""
985 981 bumped = set()
986 982 # util function (avoid attribute lookup in the loop)
987 983 phase = repo._phasecache.phase # would be faster to grab the full list
988 984 public = phases.public
989 985 cl = repo.changelog
990 986 torev = cl.index.get_rev
991 987 tonode = cl.node
992 988 obsstore = repo.obsstore
993 989 for rev in repo.revs(b'(not public()) and (not obsolete())'):
994 990 # We only evaluate mutable, non-obsolete revision
995 991 node = tonode(rev)
996 992 # (future) A cache of predecessors may worth if split is very common
997 993 for pnode in obsutil.allpredecessors(
998 994 obsstore, [node], ignoreflags=bumpedfix
999 995 ):
1000 996 prev = torev(pnode) # unfiltered! but so is phasecache
1001 997 if (prev is not None) and (phase(repo, prev) <= public):
1002 998 # we have a public predecessor
1003 999 bumped.add(rev)
1004 1000 break # Next draft!
1005 1001 return bumped
1006 1002
1007 1003
1008 1004 @cachefor(b'contentdivergent')
1009 1005 def _computecontentdivergentset(repo):
1010 1006 """the set of rev that compete to be the final successors of some revision."""
1011 1007 divergent = set()
1012 1008 obsstore = repo.obsstore
1013 1009 newermap = {}
1014 1010 tonode = repo.changelog.node
1015 1011 for rev in repo.revs(b'(not public()) - obsolete()'):
1016 1012 node = tonode(rev)
1017 1013 mark = obsstore.predecessors.get(node, ())
1018 1014 toprocess = set(mark)
1019 1015 seen = set()
1020 1016 while toprocess:
1021 1017 prec = toprocess.pop()[0]
1022 1018 if prec in seen:
1023 1019 continue # emergency cycle hanging prevention
1024 1020 seen.add(prec)
1025 1021 if prec not in newermap:
1026 1022 obsutil.successorssets(repo, prec, cache=newermap)
1027 1023 newer = [n for n in newermap[prec] if n]
1028 1024 if len(newer) > 1:
1029 1025 divergent.add(rev)
1030 1026 break
1031 1027 toprocess.update(obsstore.predecessors.get(prec, ()))
1032 1028 return divergent
1033 1029
1034 1030
1035 1031 def makefoldid(relation, user):
1036 1032
1037 1033 folddigest = hashutil.sha1(user)
1038 1034 for p in relation[0] + relation[1]:
1039 1035 folddigest.update(b'%d' % p.rev())
1040 1036 folddigest.update(p.node())
1041 1037 # Since fold only has to compete against fold for the same successors, it
1042 1038 # seems fine to use a small ID. Smaller ID save space.
1043 1039 return hex(folddigest.digest())[:8]
1044 1040
1045 1041
1046 1042 def createmarkers(
1047 1043 repo, relations, flag=0, date=None, metadata=None, operation=None
1048 1044 ):
1049 1045 """Add obsolete markers between changesets in a repo
1050 1046
1051 1047 <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}])
1052 1048 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1053 1049 containing metadata for this marker only. It is merged with the global
1054 1050 metadata specified through the `metadata` argument of this function.
1055 1051 Any string values in metadata must be UTF-8 bytes.
1056 1052
1057 1053 Trying to obsolete a public changeset will raise an exception.
1058 1054
1059 1055 Current user and date are used except if specified otherwise in the
1060 1056 metadata attribute.
1061 1057
1062 1058 This function operates within a transaction of its own, but does
1063 1059 not take any lock on the repo.
1064 1060 """
1065 1061 # prepare metadata
1066 1062 if metadata is None:
1067 1063 metadata = {}
1068 1064 if b'user' not in metadata:
1069 1065 luser = (
1070 1066 repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username()
1071 1067 )
1072 1068 metadata[b'user'] = encoding.fromlocal(luser)
1073 1069
1074 1070 # Operation metadata handling
1075 1071 useoperation = repo.ui.configbool(
1076 1072 b'experimental', b'evolution.track-operation'
1077 1073 )
1078 1074 if useoperation and operation:
1079 1075 metadata[b'operation'] = operation
1080 1076
1081 1077 # Effect flag metadata handling
1082 1078 saveeffectflag = repo.ui.configbool(
1083 1079 b'experimental', b'evolution.effect-flags'
1084 1080 )
1085 1081
1086 1082 with repo.transaction(b'add-obsolescence-marker') as tr:
1087 1083 markerargs = []
1088 1084 for rel in relations:
1089 1085 predecessors = rel[0]
1090 1086 if not isinstance(predecessors, tuple):
1091 1087 # preserve compat with old API until all caller are migrated
1092 1088 predecessors = (predecessors,)
1093 1089 if len(predecessors) > 1 and len(rel[1]) != 1:
1094 1090 msg = b'Fold markers can only have 1 successors, not %d'
1095 1091 raise error.ProgrammingError(msg % len(rel[1]))
1096 1092 foldid = None
1097 1093 foldsize = len(predecessors)
1098 1094 if 1 < foldsize:
1099 1095 foldid = makefoldid(rel, metadata[b'user'])
1100 1096 for foldidx, prec in enumerate(predecessors, 1):
1101 1097 sucs = rel[1]
1102 1098 localmetadata = metadata.copy()
1103 1099 if len(rel) > 2:
1104 1100 localmetadata.update(rel[2])
1105 1101 if foldid is not None:
1106 1102 localmetadata[b'fold-id'] = foldid
1107 1103 localmetadata[b'fold-idx'] = b'%d' % foldidx
1108 1104 localmetadata[b'fold-size'] = b'%d' % foldsize
1109 1105
1110 1106 if not prec.mutable():
1111 1107 raise error.Abort(
1112 1108 _(b"cannot obsolete public changeset: %s") % prec,
1113 1109 hint=b"see 'hg help phases' for details",
1114 1110 )
1115 1111 nprec = prec.node()
1116 1112 nsucs = tuple(s.node() for s in sucs)
1117 1113 npare = None
1118 1114 if not nsucs:
1119 1115 npare = tuple(p.node() for p in prec.parents())
1120 1116 if nprec in nsucs:
1121 1117 raise error.Abort(
1122 1118 _(b"changeset %s cannot obsolete itself") % prec
1123 1119 )
1124 1120
1125 1121 # Effect flag can be different by relation
1126 1122 if saveeffectflag:
1127 1123 # The effect flag is saved in a versioned field name for
1128 1124 # future evolution
1129 1125 effectflag = obsutil.geteffectflag(prec, sucs)
1130 1126 localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag
1131 1127
1132 1128 # Creating the marker causes the hidden cache to become
1133 1129 # invalid, which causes recomputation when we ask for
1134 1130 # prec.parents() above. Resulting in n^2 behavior. So let's
1135 1131 # prepare all of the args first, then create the markers.
1136 1132 markerargs.append((nprec, nsucs, npare, localmetadata))
1137 1133
1138 1134 for args in markerargs:
1139 1135 nprec, nsucs, npare, localmetadata = args
1140 1136 repo.obsstore.create(
1141 1137 tr,
1142 1138 nprec,
1143 1139 nsucs,
1144 1140 flag,
1145 1141 parents=npare,
1146 1142 date=date,
1147 1143 metadata=localmetadata,
1148 1144 ui=repo.ui,
1149 1145 )
1150 1146 repo.filteredrevcache.clear()
General Comments 0
You need to be logged in to leave comments. Login now