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