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