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