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