##// END OF EJS Templates
py3: use `x.hex()` instead of `pycompat.sysstr(node.hex(x))`
Manuel Jacob -
r50195:22712409 default
parent child Browse files
Show More
@@ -1,50 +1,48
1 1 """utilities to assist in working with pygit2"""
2 2
3 from mercurial.node import bin, hex, sha1nodeconstants
4
5 from mercurial import pycompat
3 from mercurial.node import bin, sha1nodeconstants
6 4
7 5 pygit2_module = None
8 6
9 7
10 8 def get_pygit2():
11 9 global pygit2_module
12 10 if pygit2_module is None:
13 11 try:
14 12 import pygit2 as pygit2_module
15 13
16 14 pygit2_module.InvalidSpecError
17 15 except (ImportError, AttributeError):
18 16 pass
19 17 return pygit2_module
20 18
21 19
22 20 def pygit2_version():
23 21 mod = get_pygit2()
24 22 v = "N/A"
25 23
26 24 if mod:
27 25 try:
28 26 v = mod.__version__
29 27 except AttributeError:
30 28 pass
31 29
32 30 return b"(pygit2 %s)" % v.encode("utf-8")
33 31
34 32
35 33 def togitnode(n):
36 34 """Wrapper to convert a Mercurial binary node to a unicode hexlified node.
37 35
38 36 pygit2 and sqlite both need nodes as strings, not bytes.
39 37 """
40 38 assert len(n) == 20
41 return pycompat.sysstr(hex(n))
39 return n.hex()
42 40
43 41
44 42 def fromgitnode(n):
45 43 """Opposite of togitnode."""
46 44 assert len(n) == 40
47 45 return bin(n)
48 46
49 47
50 48 nullgit = togitnode(sha1nodeconstants.nullid)
@@ -1,880 +1,880
1 1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8
9 9 import struct
10 10
11 11 from .node import (
12 12 bin,
13 13 hex,
14 14 nullrev,
15 15 )
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 obsolete,
20 20 pycompat,
21 21 scmutil,
22 22 util,
23 23 )
24 24 from .utils import (
25 25 repoviewutil,
26 26 stringutil,
27 27 )
28 28
29 29 if pycompat.TYPE_CHECKING:
30 30 from typing import (
31 31 Any,
32 32 Callable,
33 33 Dict,
34 34 Iterable,
35 35 List,
36 36 Optional,
37 37 Set,
38 38 Tuple,
39 39 Union,
40 40 )
41 41 from . import localrepo
42 42
43 43 assert any(
44 44 (
45 45 Any,
46 46 Callable,
47 47 Dict,
48 48 Iterable,
49 49 List,
50 50 Optional,
51 51 Set,
52 52 Tuple,
53 53 Union,
54 54 localrepo,
55 55 )
56 56 )
57 57
58 58 subsettable = repoviewutil.subsettable
59 59
60 60 calcsize = struct.calcsize
61 61 pack_into = struct.pack_into
62 62 unpack_from = struct.unpack_from
63 63
64 64
65 65 class BranchMapCache:
66 66 """mapping of filtered views of repo with their branchcache"""
67 67
68 68 def __init__(self):
69 69 self._per_filter = {}
70 70
71 71 def __getitem__(self, repo):
72 72 self.updatecache(repo)
73 73 return self._per_filter[repo.filtername]
74 74
75 75 def updatecache(self, repo):
76 76 """Update the cache for the given filtered view on a repository"""
77 77 # This can trigger updates for the caches for subsets of the filtered
78 78 # view, e.g. when there is no cache for this filtered view or the cache
79 79 # is stale.
80 80
81 81 cl = repo.changelog
82 82 filtername = repo.filtername
83 83 bcache = self._per_filter.get(filtername)
84 84 if bcache is None or not bcache.validfor(repo):
85 85 # cache object missing or cache object stale? Read from disk
86 86 bcache = branchcache.fromfile(repo)
87 87
88 88 revs = []
89 89 if bcache is None:
90 90 # no (fresh) cache available anymore, perhaps we can re-use
91 91 # the cache for a subset, then extend that to add info on missing
92 92 # revisions.
93 93 subsetname = subsettable.get(filtername)
94 94 if subsetname is not None:
95 95 subset = repo.filtered(subsetname)
96 96 bcache = self[subset].copy()
97 97 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
98 98 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
99 99 else:
100 100 # nothing to fall back on, start empty.
101 101 bcache = branchcache(repo)
102 102
103 103 revs.extend(cl.revs(start=bcache.tiprev + 1))
104 104 if revs:
105 105 bcache.update(repo, revs)
106 106
107 107 assert bcache.validfor(repo), filtername
108 108 self._per_filter[repo.filtername] = bcache
109 109
110 110 def replace(self, repo, remotebranchmap):
111 111 """Replace the branchmap cache for a repo with a branch mapping.
112 112
113 113 This is likely only called during clone with a branch map from a
114 114 remote.
115 115
116 116 """
117 117 cl = repo.changelog
118 118 clrev = cl.rev
119 119 clbranchinfo = cl.branchinfo
120 120 rbheads = []
121 121 closed = set()
122 122 for bheads in remotebranchmap.values():
123 123 rbheads += bheads
124 124 for h in bheads:
125 125 r = clrev(h)
126 126 b, c = clbranchinfo(r)
127 127 if c:
128 128 closed.add(h)
129 129
130 130 if rbheads:
131 131 rtiprev = max((int(clrev(node)) for node in rbheads))
132 132 cache = branchcache(
133 133 repo,
134 134 remotebranchmap,
135 135 repo[rtiprev].node(),
136 136 rtiprev,
137 137 closednodes=closed,
138 138 )
139 139
140 140 # Try to stick it as low as possible
141 141 # filter above served are unlikely to be fetch from a clone
142 142 for candidate in (b'base', b'immutable', b'served'):
143 143 rview = repo.filtered(candidate)
144 144 if cache.validfor(rview):
145 145 self._per_filter[candidate] = cache
146 146 cache.write(rview)
147 147 return
148 148
149 149 def clear(self):
150 150 self._per_filter.clear()
151 151
152 152 def write_delayed(self, repo):
153 153 unfi = repo.unfiltered()
154 154 for filtername, cache in self._per_filter.items():
155 155 if cache._delayed:
156 156 repo = unfi.filtered(filtername)
157 157 cache.write(repo)
158 158
159 159
160 160 def _unknownnode(node):
161 161 """raises ValueError when branchcache found a node which does not exists"""
162 raise ValueError('node %s does not exist' % pycompat.sysstr(hex(node)))
162 raise ValueError('node %s does not exist' % node.hex())
163 163
164 164
165 165 def _branchcachedesc(repo):
166 166 if repo.filtername is not None:
167 167 return b'branch cache (%s)' % repo.filtername
168 168 else:
169 169 return b'branch cache'
170 170
171 171
172 172 class branchcache:
173 173 """A dict like object that hold branches heads cache.
174 174
175 175 This cache is used to avoid costly computations to determine all the
176 176 branch heads of a repo.
177 177
178 178 The cache is serialized on disk in the following format:
179 179
180 180 <tip hex node> <tip rev number> [optional filtered repo hex hash]
181 181 <branch head hex node> <open/closed state> <branch name>
182 182 <branch head hex node> <open/closed state> <branch name>
183 183 ...
184 184
185 185 The first line is used to check if the cache is still valid. If the
186 186 branch cache is for a filtered repo view, an optional third hash is
187 187 included that hashes the hashes of all filtered and obsolete revisions.
188 188
189 189 The open/closed state is represented by a single letter 'o' or 'c'.
190 190 This field can be used to avoid changelog reads when determining if a
191 191 branch head closes a branch or not.
192 192 """
193 193
194 194 def __init__(
195 195 self,
196 196 repo,
197 197 entries=(),
198 198 tipnode=None,
199 199 tiprev=nullrev,
200 200 filteredhash=None,
201 201 closednodes=None,
202 202 hasnode=None,
203 203 ):
204 204 # type: (localrepo.localrepository, Union[Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]], bytes, int, Optional[bytes], Optional[Set[bytes]], Optional[Callable[[bytes], bool]]) -> None
205 205 """hasnode is a function which can be used to verify whether changelog
206 206 has a given node or not. If it's not provided, we assume that every node
207 207 we have exists in changelog"""
208 208 self._repo = repo
209 209 self._delayed = False
210 210 if tipnode is None:
211 211 self.tipnode = repo.nullid
212 212 else:
213 213 self.tipnode = tipnode
214 214 self.tiprev = tiprev
215 215 self.filteredhash = filteredhash
216 216 # closednodes is a set of nodes that close their branch. If the branch
217 217 # cache has been updated, it may contain nodes that are no longer
218 218 # heads.
219 219 if closednodes is None:
220 220 self._closednodes = set()
221 221 else:
222 222 self._closednodes = closednodes
223 223 self._entries = dict(entries)
224 224 # whether closed nodes are verified or not
225 225 self._closedverified = False
226 226 # branches for which nodes are verified
227 227 self._verifiedbranches = set()
228 228 self._hasnode = hasnode
229 229 if self._hasnode is None:
230 230 self._hasnode = lambda x: True
231 231
232 232 def _verifyclosed(self):
233 233 """verify the closed nodes we have"""
234 234 if self._closedverified:
235 235 return
236 236 for node in self._closednodes:
237 237 if not self._hasnode(node):
238 238 _unknownnode(node)
239 239
240 240 self._closedverified = True
241 241
242 242 def _verifybranch(self, branch):
243 243 """verify head nodes for the given branch."""
244 244 if branch not in self._entries or branch in self._verifiedbranches:
245 245 return
246 246 for n in self._entries[branch]:
247 247 if not self._hasnode(n):
248 248 _unknownnode(n)
249 249
250 250 self._verifiedbranches.add(branch)
251 251
252 252 def _verifyall(self):
253 253 """verifies nodes of all the branches"""
254 254 needverification = set(self._entries.keys()) - self._verifiedbranches
255 255 for b in needverification:
256 256 self._verifybranch(b)
257 257
258 258 def __iter__(self):
259 259 return iter(self._entries)
260 260
261 261 def __setitem__(self, key, value):
262 262 self._entries[key] = value
263 263
264 264 def __getitem__(self, key):
265 265 self._verifybranch(key)
266 266 return self._entries[key]
267 267
268 268 def __contains__(self, key):
269 269 self._verifybranch(key)
270 270 return key in self._entries
271 271
272 272 def iteritems(self):
273 273 for k, v in self._entries.items():
274 274 self._verifybranch(k)
275 275 yield k, v
276 276
277 277 items = iteritems
278 278
279 279 def hasbranch(self, label):
280 280 """checks whether a branch of this name exists or not"""
281 281 self._verifybranch(label)
282 282 return label in self._entries
283 283
284 284 @classmethod
285 285 def fromfile(cls, repo):
286 286 f = None
287 287 try:
288 288 f = repo.cachevfs(cls._filename(repo))
289 289 lineiter = iter(f)
290 290 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
291 291 last, lrev = cachekey[:2]
292 292 last, lrev = bin(last), int(lrev)
293 293 filteredhash = None
294 294 hasnode = repo.changelog.hasnode
295 295 if len(cachekey) > 2:
296 296 filteredhash = bin(cachekey[2])
297 297 bcache = cls(
298 298 repo,
299 299 tipnode=last,
300 300 tiprev=lrev,
301 301 filteredhash=filteredhash,
302 302 hasnode=hasnode,
303 303 )
304 304 if not bcache.validfor(repo):
305 305 # invalidate the cache
306 306 raise ValueError('tip differs')
307 307 bcache.load(repo, lineiter)
308 308 except (IOError, OSError):
309 309 return None
310 310
311 311 except Exception as inst:
312 312 if repo.ui.debugflag:
313 313 msg = b'invalid %s: %s\n'
314 314 repo.ui.debug(
315 315 msg
316 316 % (
317 317 _branchcachedesc(repo),
318 318 stringutil.forcebytestr(inst),
319 319 )
320 320 )
321 321 bcache = None
322 322
323 323 finally:
324 324 if f:
325 325 f.close()
326 326
327 327 return bcache
328 328
329 329 def load(self, repo, lineiter):
330 330 """fully loads the branchcache by reading from the file using the line
331 331 iterator passed"""
332 332 for line in lineiter:
333 333 line = line.rstrip(b'\n')
334 334 if not line:
335 335 continue
336 336 node, state, label = line.split(b" ", 2)
337 337 if state not in b'oc':
338 338 raise ValueError('invalid branch state')
339 339 label = encoding.tolocal(label.strip())
340 340 node = bin(node)
341 341 self._entries.setdefault(label, []).append(node)
342 342 if state == b'c':
343 343 self._closednodes.add(node)
344 344
345 345 @staticmethod
346 346 def _filename(repo):
347 347 """name of a branchcache file for a given repo or repoview"""
348 348 filename = b"branch2"
349 349 if repo.filtername:
350 350 filename = b'%s-%s' % (filename, repo.filtername)
351 351 return filename
352 352
353 353 def validfor(self, repo):
354 354 """check that cache contents are valid for (a subset of) this repo
355 355
356 356 - False when the order of changesets changed or if we detect a strip.
357 357 - True when cache is up-to-date for the current repo or its subset."""
358 358 try:
359 359 node = repo.changelog.node(self.tiprev)
360 360 except IndexError:
361 361 # changesets were stripped and now we don't even have enough to
362 362 # find tiprev
363 363 return False
364 364 if self.tipnode != node:
365 365 # tiprev doesn't correspond to tipnode: repo was stripped, or this
366 366 # repo has a different order of changesets
367 367 return False
368 368 tiphash = scmutil.filteredhash(repo, self.tiprev, needobsolete=True)
369 369 # hashes don't match if this repo view has a different set of filtered
370 370 # revisions (e.g. due to phase changes) or obsolete revisions (e.g.
371 371 # history was rewritten)
372 372 return self.filteredhash == tiphash
373 373
374 374 def _branchtip(self, heads):
375 375 """Return tuple with last open head in heads and false,
376 376 otherwise return last closed head and true."""
377 377 tip = heads[-1]
378 378 closed = True
379 379 for h in reversed(heads):
380 380 if h not in self._closednodes:
381 381 tip = h
382 382 closed = False
383 383 break
384 384 return tip, closed
385 385
386 386 def branchtip(self, branch):
387 387 """Return the tipmost open head on branch head, otherwise return the
388 388 tipmost closed head on branch.
389 389 Raise KeyError for unknown branch."""
390 390 return self._branchtip(self[branch])[0]
391 391
392 392 def iteropen(self, nodes):
393 393 return (n for n in nodes if n not in self._closednodes)
394 394
395 395 def branchheads(self, branch, closed=False):
396 396 self._verifybranch(branch)
397 397 heads = self._entries[branch]
398 398 if not closed:
399 399 heads = list(self.iteropen(heads))
400 400 return heads
401 401
402 402 def iterbranches(self):
403 403 for bn, heads in self.items():
404 404 yield (bn, heads) + self._branchtip(heads)
405 405
406 406 def iterheads(self):
407 407 """returns all the heads"""
408 408 self._verifyall()
409 409 return self._entries.values()
410 410
411 411 def copy(self):
412 412 """return an deep copy of the branchcache object"""
413 413 return type(self)(
414 414 self._repo,
415 415 self._entries,
416 416 self.tipnode,
417 417 self.tiprev,
418 418 self.filteredhash,
419 419 self._closednodes,
420 420 )
421 421
422 422 def write(self, repo):
423 423 tr = repo.currenttransaction()
424 424 if not getattr(tr, 'finalized', True):
425 425 # Avoid premature writing.
426 426 #
427 427 # (The cache warming setup by localrepo will update the file later.)
428 428 self._delayed = True
429 429 return
430 430 try:
431 431 filename = self._filename(repo)
432 432 with repo.cachevfs(filename, b"w", atomictemp=True) as f:
433 433 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
434 434 if self.filteredhash is not None:
435 435 cachekey.append(hex(self.filteredhash))
436 436 f.write(b" ".join(cachekey) + b'\n')
437 437 nodecount = 0
438 438 for label, nodes in sorted(self._entries.items()):
439 439 label = encoding.fromlocal(label)
440 440 for node in nodes:
441 441 nodecount += 1
442 442 if node in self._closednodes:
443 443 state = b'c'
444 444 else:
445 445 state = b'o'
446 446 f.write(b"%s %s %s\n" % (hex(node), state, label))
447 447 repo.ui.log(
448 448 b'branchcache',
449 449 b'wrote %s with %d labels and %d nodes\n',
450 450 _branchcachedesc(repo),
451 451 len(self._entries),
452 452 nodecount,
453 453 )
454 454 self._delayed = False
455 455 except (IOError, OSError, error.Abort) as inst:
456 456 # Abort may be raised by read only opener, so log and continue
457 457 repo.ui.debug(
458 458 b"couldn't write branch cache: %s\n"
459 459 % stringutil.forcebytestr(inst)
460 460 )
461 461
462 462 def update(self, repo, revgen):
463 463 """Given a branchhead cache, self, that may have extra nodes or be
464 464 missing heads, and a generator of nodes that are strictly a superset of
465 465 heads missing, this function updates self to be correct.
466 466 """
467 467 starttime = util.timer()
468 468 cl = repo.changelog
469 469 # collect new branch entries
470 470 newbranches = {}
471 471 getbranchinfo = repo.revbranchcache().branchinfo
472 472 for r in revgen:
473 473 branch, closesbranch = getbranchinfo(r)
474 474 newbranches.setdefault(branch, []).append(r)
475 475 if closesbranch:
476 476 self._closednodes.add(cl.node(r))
477 477
478 478 # new tip revision which we found after iterating items from new
479 479 # branches
480 480 ntiprev = self.tiprev
481 481
482 482 # Delay fetching the topological heads until they are needed.
483 483 # A repository without non-continous branches can skip this part.
484 484 topoheads = None
485 485
486 486 # If a changeset is visible, its parents must be visible too, so
487 487 # use the faster unfiltered parent accessor.
488 488 parentrevs = repo.unfiltered().changelog.parentrevs
489 489
490 490 # Faster than using ctx.obsolete()
491 491 obsrevs = obsolete.getrevs(repo, b'obsolete')
492 492
493 493 for branch, newheadrevs in newbranches.items():
494 494 # For every branch, compute the new branchheads.
495 495 # A branchhead is a revision such that no descendant is on
496 496 # the same branch.
497 497 #
498 498 # The branchheads are computed iteratively in revision order.
499 499 # This ensures topological order, i.e. parents are processed
500 500 # before their children. Ancestors are inclusive here, i.e.
501 501 # any revision is an ancestor of itself.
502 502 #
503 503 # Core observations:
504 504 # - The current revision is always a branchhead for the
505 505 # repository up to that point.
506 506 # - It is the first revision of the branch if and only if
507 507 # there was no branchhead before. In that case, it is the
508 508 # only branchhead as there are no possible ancestors on
509 509 # the same branch.
510 510 # - If a parent is on the same branch, a branchhead can
511 511 # only be an ancestor of that parent, if it is parent
512 512 # itself. Otherwise it would have been removed as ancestor
513 513 # of that parent before.
514 514 # - Therefore, if all parents are on the same branch, they
515 515 # can just be removed from the branchhead set.
516 516 # - If one parent is on the same branch and the other is not
517 517 # and there was exactly one branchhead known, the existing
518 518 # branchhead can only be an ancestor if it is the parent.
519 519 # Otherwise it would have been removed as ancestor of
520 520 # the parent before. The other parent therefore can't have
521 521 # a branchhead as ancestor.
522 522 # - In all other cases, the parents on different branches
523 523 # could have a branchhead as ancestor. Those parents are
524 524 # kept in the "uncertain" set. If all branchheads are also
525 525 # topological heads, they can't have descendants and further
526 526 # checks can be skipped. Otherwise, the ancestors of the
527 527 # "uncertain" set are removed from branchheads.
528 528 # This computation is heavy and avoided if at all possible.
529 529 bheads = self._entries.get(branch, [])
530 530 bheadset = {cl.rev(node) for node in bheads}
531 531 uncertain = set()
532 532 for newrev in sorted(newheadrevs):
533 533 if newrev in obsrevs:
534 534 # We ignore obsolete changesets as they shouldn't be
535 535 # considered heads.
536 536 continue
537 537
538 538 if not bheadset:
539 539 bheadset.add(newrev)
540 540 continue
541 541
542 542 parents = [p for p in parentrevs(newrev) if p != nullrev]
543 543 samebranch = set()
544 544 otherbranch = set()
545 545 obsparents = set()
546 546 for p in parents:
547 547 if p in obsrevs:
548 548 # We ignored this obsolete changeset earlier, but now
549 549 # that it has non-ignored children, we need to make
550 550 # sure their ancestors are not considered heads. To
551 551 # achieve that, we will simply treat this obsolete
552 552 # changeset as a parent from other branch.
553 553 obsparents.add(p)
554 554 elif p in bheadset or getbranchinfo(p)[0] == branch:
555 555 samebranch.add(p)
556 556 else:
557 557 otherbranch.add(p)
558 558 if not (len(bheadset) == len(samebranch) == 1):
559 559 uncertain.update(otherbranch)
560 560 uncertain.update(obsparents)
561 561 bheadset.difference_update(samebranch)
562 562 bheadset.add(newrev)
563 563
564 564 if uncertain:
565 565 if topoheads is None:
566 566 topoheads = set(cl.headrevs())
567 567 if bheadset - topoheads:
568 568 floorrev = min(bheadset)
569 569 if floorrev <= max(uncertain):
570 570 ancestors = set(cl.ancestors(uncertain, floorrev))
571 571 bheadset -= ancestors
572 572 if bheadset:
573 573 self[branch] = [cl.node(rev) for rev in sorted(bheadset)]
574 574 tiprev = max(newheadrevs)
575 575 if tiprev > ntiprev:
576 576 ntiprev = tiprev
577 577
578 578 if ntiprev > self.tiprev:
579 579 self.tiprev = ntiprev
580 580 self.tipnode = cl.node(ntiprev)
581 581
582 582 if not self.validfor(repo):
583 583 # old cache key is now invalid for the repo, but we've just updated
584 584 # the cache and we assume it's valid, so let's make the cache key
585 585 # valid as well by recomputing it from the cached data
586 586 self.tipnode = repo.nullid
587 587 self.tiprev = nullrev
588 588 for heads in self.iterheads():
589 589 if not heads:
590 590 # all revisions on a branch are obsolete
591 591 continue
592 592 # note: tiprev is not necessarily the tip revision of repo,
593 593 # because the tip could be obsolete (i.e. not a head)
594 594 tiprev = max(cl.rev(node) for node in heads)
595 595 if tiprev > self.tiprev:
596 596 self.tipnode = cl.node(tiprev)
597 597 self.tiprev = tiprev
598 598 self.filteredhash = scmutil.filteredhash(
599 599 repo, self.tiprev, needobsolete=True
600 600 )
601 601
602 602 duration = util.timer() - starttime
603 603 repo.ui.log(
604 604 b'branchcache',
605 605 b'updated %s in %.4f seconds\n',
606 606 _branchcachedesc(repo),
607 607 duration,
608 608 )
609 609
610 610 self.write(repo)
611 611
612 612
613 613 class remotebranchcache(branchcache):
614 614 """Branchmap info for a remote connection, should not write locally"""
615 615
616 616 def write(self, repo):
617 617 pass
618 618
619 619
620 620 # Revision branch info cache
621 621
622 622 _rbcversion = b'-v1'
623 623 _rbcnames = b'rbc-names' + _rbcversion
624 624 _rbcrevs = b'rbc-revs' + _rbcversion
625 625 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
626 626 _rbcrecfmt = b'>4sI'
627 627 _rbcrecsize = calcsize(_rbcrecfmt)
628 628 _rbcmininc = 64 * _rbcrecsize
629 629 _rbcnodelen = 4
630 630 _rbcbranchidxmask = 0x7FFFFFFF
631 631 _rbccloseflag = 0x80000000
632 632
633 633
634 634 class revbranchcache:
635 635 """Persistent cache, mapping from revision number to branch name and close.
636 636 This is a low level cache, independent of filtering.
637 637
638 638 Branch names are stored in rbc-names in internal encoding separated by 0.
639 639 rbc-names is append-only, and each branch name is only stored once and will
640 640 thus have a unique index.
641 641
642 642 The branch info for each revision is stored in rbc-revs as constant size
643 643 records. The whole file is read into memory, but it is only 'parsed' on
644 644 demand. The file is usually append-only but will be truncated if repo
645 645 modification is detected.
646 646 The record for each revision contains the first 4 bytes of the
647 647 corresponding node hash, and the record is only used if it still matches.
648 648 Even a completely trashed rbc-revs fill thus still give the right result
649 649 while converging towards full recovery ... assuming no incorrectly matching
650 650 node hashes.
651 651 The record also contains 4 bytes where 31 bits contains the index of the
652 652 branch and the last bit indicate that it is a branch close commit.
653 653 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
654 654 and will grow with it but be 1/8th of its size.
655 655 """
656 656
657 657 def __init__(self, repo, readonly=True):
658 658 assert repo.filtername is None
659 659 self._repo = repo
660 660 self._names = [] # branch names in local encoding with static index
661 661 self._rbcrevs = bytearray()
662 662 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
663 663 try:
664 664 bndata = repo.cachevfs.read(_rbcnames)
665 665 self._rbcsnameslen = len(bndata) # for verification before writing
666 666 if bndata:
667 667 self._names = [
668 668 encoding.tolocal(bn) for bn in bndata.split(b'\0')
669 669 ]
670 670 except (IOError, OSError):
671 671 if readonly:
672 672 # don't try to use cache - fall back to the slow path
673 673 self.branchinfo = self._branchinfo
674 674
675 675 if self._names:
676 676 try:
677 677 data = repo.cachevfs.read(_rbcrevs)
678 678 self._rbcrevs[:] = data
679 679 except (IOError, OSError) as inst:
680 680 repo.ui.debug(
681 681 b"couldn't read revision branch cache: %s\n"
682 682 % stringutil.forcebytestr(inst)
683 683 )
684 684 # remember number of good records on disk
685 685 self._rbcrevslen = min(
686 686 len(self._rbcrevs) // _rbcrecsize, len(repo.changelog)
687 687 )
688 688 if self._rbcrevslen == 0:
689 689 self._names = []
690 690 self._rbcnamescount = len(self._names) # number of names read at
691 691 # _rbcsnameslen
692 692
693 693 def _clear(self):
694 694 self._rbcsnameslen = 0
695 695 del self._names[:]
696 696 self._rbcnamescount = 0
697 697 self._rbcrevslen = len(self._repo.changelog)
698 698 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
699 699 util.clearcachedproperty(self, b'_namesreverse')
700 700
701 701 @util.propertycache
702 702 def _namesreverse(self):
703 703 return {b: r for r, b in enumerate(self._names)}
704 704
705 705 def branchinfo(self, rev):
706 706 """Return branch name and close flag for rev, using and updating
707 707 persistent cache."""
708 708 changelog = self._repo.changelog
709 709 rbcrevidx = rev * _rbcrecsize
710 710
711 711 # avoid negative index, changelog.read(nullrev) is fast without cache
712 712 if rev == nullrev:
713 713 return changelog.branchinfo(rev)
714 714
715 715 # if requested rev isn't allocated, grow and cache the rev info
716 716 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
717 717 return self._branchinfo(rev)
718 718
719 719 # fast path: extract data from cache, use it if node is matching
720 720 reponode = changelog.node(rev)[:_rbcnodelen]
721 721 cachenode, branchidx = unpack_from(
722 722 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx
723 723 )
724 724 close = bool(branchidx & _rbccloseflag)
725 725 if close:
726 726 branchidx &= _rbcbranchidxmask
727 727 if cachenode == b'\0\0\0\0':
728 728 pass
729 729 elif cachenode == reponode:
730 730 try:
731 731 return self._names[branchidx], close
732 732 except IndexError:
733 733 # recover from invalid reference to unknown branch
734 734 self._repo.ui.debug(
735 735 b"referenced branch names not found"
736 736 b" - rebuilding revision branch cache from scratch\n"
737 737 )
738 738 self._clear()
739 739 else:
740 740 # rev/node map has changed, invalidate the cache from here up
741 741 self._repo.ui.debug(
742 742 b"history modification detected - truncating "
743 743 b"revision branch cache to revision %d\n" % rev
744 744 )
745 745 truncate = rbcrevidx + _rbcrecsize
746 746 del self._rbcrevs[truncate:]
747 747 self._rbcrevslen = min(self._rbcrevslen, truncate)
748 748
749 749 # fall back to slow path and make sure it will be written to disk
750 750 return self._branchinfo(rev)
751 751
752 752 def _branchinfo(self, rev):
753 753 """Retrieve branch info from changelog and update _rbcrevs"""
754 754 changelog = self._repo.changelog
755 755 b, close = changelog.branchinfo(rev)
756 756 if b in self._namesreverse:
757 757 branchidx = self._namesreverse[b]
758 758 else:
759 759 branchidx = len(self._names)
760 760 self._names.append(b)
761 761 self._namesreverse[b] = branchidx
762 762 reponode = changelog.node(rev)
763 763 if close:
764 764 branchidx |= _rbccloseflag
765 765 self._setcachedata(rev, reponode, branchidx)
766 766 return b, close
767 767
768 768 def setdata(self, rev, changelogrevision):
769 769 """add new data information to the cache"""
770 770 branch, close = changelogrevision.branchinfo
771 771
772 772 if branch in self._namesreverse:
773 773 branchidx = self._namesreverse[branch]
774 774 else:
775 775 branchidx = len(self._names)
776 776 self._names.append(branch)
777 777 self._namesreverse[branch] = branchidx
778 778 if close:
779 779 branchidx |= _rbccloseflag
780 780 self._setcachedata(rev, self._repo.changelog.node(rev), branchidx)
781 781 # If no cache data were readable (non exists, bad permission, etc)
782 782 # the cache was bypassing itself by setting:
783 783 #
784 784 # self.branchinfo = self._branchinfo
785 785 #
786 786 # Since we now have data in the cache, we need to drop this bypassing.
787 787 if 'branchinfo' in vars(self):
788 788 del self.branchinfo
789 789
790 790 def _setcachedata(self, rev, node, branchidx):
791 791 """Writes the node's branch data to the in-memory cache data."""
792 792 if rev == nullrev:
793 793 return
794 794 rbcrevidx = rev * _rbcrecsize
795 795 requiredsize = rbcrevidx + _rbcrecsize
796 796 rbccur = len(self._rbcrevs)
797 797 if rbccur < requiredsize:
798 798 # bytearray doesn't allocate extra space at least in Python 3.7.
799 799 # When multiple changesets are added in a row, precise resize would
800 800 # result in quadratic complexity. Overallocate to compensate by
801 801 # use the classic doubling technique for dynamic arrays instead.
802 802 # If there was a gap in the map before, less space will be reserved.
803 803 self._rbcrevs.extend(b'\0' * max(_rbcmininc, requiredsize))
804 804 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
805 805 self._rbcrevslen = min(self._rbcrevslen, rev)
806 806
807 807 tr = self._repo.currenttransaction()
808 808 if tr:
809 809 tr.addfinalize(b'write-revbranchcache', self.write)
810 810
811 811 def write(self, tr=None):
812 812 """Save branch cache if it is dirty."""
813 813 repo = self._repo
814 814 wlock = None
815 815 step = b''
816 816 try:
817 817 # write the new names
818 818 if self._rbcnamescount < len(self._names):
819 819 wlock = repo.wlock(wait=False)
820 820 step = b' names'
821 821 self._writenames(repo)
822 822
823 823 # write the new revs
824 824 start = self._rbcrevslen * _rbcrecsize
825 825 if start != len(self._rbcrevs):
826 826 step = b''
827 827 if wlock is None:
828 828 wlock = repo.wlock(wait=False)
829 829 self._writerevs(repo, start)
830 830
831 831 except (IOError, OSError, error.Abort, error.LockError) as inst:
832 832 repo.ui.debug(
833 833 b"couldn't write revision branch cache%s: %s\n"
834 834 % (step, stringutil.forcebytestr(inst))
835 835 )
836 836 finally:
837 837 if wlock is not None:
838 838 wlock.release()
839 839
840 840 def _writenames(self, repo):
841 841 """write the new branch names to revbranchcache"""
842 842 if self._rbcnamescount != 0:
843 843 f = repo.cachevfs.open(_rbcnames, b'ab')
844 844 if f.tell() == self._rbcsnameslen:
845 845 f.write(b'\0')
846 846 else:
847 847 f.close()
848 848 repo.ui.debug(b"%s changed - rewriting it\n" % _rbcnames)
849 849 self._rbcnamescount = 0
850 850 self._rbcrevslen = 0
851 851 if self._rbcnamescount == 0:
852 852 # before rewriting names, make sure references are removed
853 853 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
854 854 f = repo.cachevfs.open(_rbcnames, b'wb')
855 855 f.write(
856 856 b'\0'.join(
857 857 encoding.fromlocal(b)
858 858 for b in self._names[self._rbcnamescount :]
859 859 )
860 860 )
861 861 self._rbcsnameslen = f.tell()
862 862 f.close()
863 863 self._rbcnamescount = len(self._names)
864 864
865 865 def _writerevs(self, repo, start):
866 866 """write the new revs to revbranchcache"""
867 867 revs = min(len(repo.changelog), len(self._rbcrevs) // _rbcrecsize)
868 868 with repo.cachevfs.open(_rbcrevs, b'ab') as f:
869 869 if f.tell() != start:
870 870 repo.ui.debug(
871 871 b"truncating cache/%s to %d\n" % (_rbcrevs, start)
872 872 )
873 873 f.seek(start)
874 874 if f.tell() != start:
875 875 start = 0
876 876 f.seek(start)
877 877 f.truncate()
878 878 end = revs * _rbcrecsize
879 879 f.write(self._rbcrevs[start:end])
880 880 self._rbcrevslen = revs
@@ -1,1149 +1,1147
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete marker handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewrite operations, and help
18 18 building new tools to reconcile conflicting rewrite actions. To
19 19 facilitate conflict resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called a "predecessor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a predecessor are called "successor markers of X" because they hold
26 26 information about the successors of X. Markers that use changeset Y as
27 27 a successors are call "predecessor markers of Y" because they hold
28 28 information about the predecessors of Y.
29 29
30 30 Examples:
31 31
32 32 - When changeset A is replaced by changeset A', one marker is stored:
33 33
34 34 (A, (A',))
35 35
36 36 - When changesets A and B are folded into a new changeset C, two markers are
37 37 stored:
38 38
39 39 (A, (C,)) and (B, (C,))
40 40
41 41 - When changeset A is simply "pruned" from the graph, a marker is created:
42 42
43 43 (A, ())
44 44
45 45 - When changeset A is split into B and C, a single marker is used:
46 46
47 47 (A, (B, C))
48 48
49 49 We use a single marker to distinguish the "split" case from the "divergence"
50 50 case. If two independent operations rewrite the same changeset A in to A' and
51 51 A'', we have an error case: divergent rewriting. We can detect it because
52 52 two markers will be created independently:
53 53
54 54 (A, (B,)) and (A, (C,))
55 55
56 56 Format
57 57 ------
58 58
59 59 Markers are stored in an append-only file stored in
60 60 '.hg/store/obsstore'.
61 61
62 62 The file starts with a version header:
63 63
64 64 - 1 unsigned byte: version number, starting at zero.
65 65
66 66 The header is followed by the markers. Marker format depend of the version. See
67 67 comment associated with each format for details.
68 68
69 69 """
70 70
71 71 import binascii
72 72 import 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 binascii.Error:
249 249 # if content cannot be translated to nodeid drop the data.
250 250 parents = None
251 251
252 252 metadata = tuple(sorted(metadata.items()))
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 meta.items():
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 _fm1metapair = b'BB'
343 343 _fm1metapairsize = _calcsize(_fm1metapair)
344 344
345 345
346 346 def _fm1purereadmarkers(data, off, stop):
347 347 # make some global constants local for performance
348 348 noneflag = _fm1parentnone
349 349 sha2flag = usingsha256
350 350 sha1size = _fm1nodesha1size
351 351 sha2size = _fm1nodesha256size
352 352 sha1fmt = _fm1nodesha1
353 353 sha2fmt = _fm1nodesha256
354 354 metasize = _fm1metapairsize
355 355 metafmt = _fm1metapair
356 356 fsize = _fm1fsize
357 357 unpack = _unpack
358 358
359 359 # Loop on markers
360 360 ufixed = struct.Struct(_fm1fixed).unpack
361 361
362 362 while off < stop:
363 363 # read fixed part
364 364 o1 = off + fsize
365 365 t, secs, tz, flags, numsuc, numpar, nummeta = ufixed(data[off:o1])
366 366
367 367 if flags & sha2flag:
368 368 nodefmt = sha2fmt
369 369 nodesize = sha2size
370 370 else:
371 371 nodefmt = sha1fmt
372 372 nodesize = sha1size
373 373
374 374 (prec,) = unpack(nodefmt, data[o1 : o1 + nodesize])
375 375 o1 += nodesize
376 376
377 377 # read 0 or more successors
378 378 if numsuc == 1:
379 379 o2 = o1 + nodesize
380 380 sucs = (data[o1:o2],)
381 381 else:
382 382 o2 = o1 + nodesize * numsuc
383 383 sucs = unpack(nodefmt * numsuc, data[o1:o2])
384 384
385 385 # read parents
386 386 if numpar == noneflag:
387 387 o3 = o2
388 388 parents = None
389 389 elif numpar == 1:
390 390 o3 = o2 + nodesize
391 391 parents = (data[o2:o3],)
392 392 else:
393 393 o3 = o2 + nodesize * numpar
394 394 parents = unpack(nodefmt * numpar, data[o2:o3])
395 395
396 396 # read metadata
397 397 off = o3 + metasize * nummeta
398 398 metapairsize = unpack(b'>' + (metafmt * nummeta), data[o3:off])
399 399 metadata = []
400 400 for idx in range(0, len(metapairsize), 2):
401 401 o1 = off + metapairsize[idx]
402 402 o2 = o1 + metapairsize[idx + 1]
403 403 metadata.append((data[off:o1], data[o1:o2]))
404 404 off = o2
405 405
406 406 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
407 407
408 408
409 409 def _fm1encodeonemarker(marker):
410 410 pre, sucs, flags, metadata, date, parents = marker
411 411 # determine node size
412 412 _fm1node = _fm1nodesha1
413 413 if flags & usingsha256:
414 414 _fm1node = _fm1nodesha256
415 415 numsuc = len(sucs)
416 416 numextranodes = 1 + numsuc
417 417 if parents is None:
418 418 numpar = _fm1parentnone
419 419 else:
420 420 numpar = len(parents)
421 421 numextranodes += numpar
422 422 formatnodes = _fm1node * numextranodes
423 423 formatmeta = _fm1metapair * len(metadata)
424 424 format = _fm1fixed + formatnodes + formatmeta
425 425 # tz is stored in minutes so we divide by 60
426 426 tz = date[1] // 60
427 427 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
428 428 data.extend(sucs)
429 429 if parents is not None:
430 430 data.extend(parents)
431 431 totalsize = _calcsize(format)
432 432 for key, value in metadata:
433 433 lk = len(key)
434 434 lv = len(value)
435 435 if lk > 255:
436 436 msg = (
437 437 b'obsstore metadata key cannot be longer than 255 bytes'
438 438 b' (key "%s" is %u bytes)'
439 439 ) % (key, lk)
440 440 raise error.ProgrammingError(msg)
441 441 if lv > 255:
442 442 msg = (
443 443 b'obsstore metadata value cannot be longer than 255 bytes'
444 444 b' (value "%s" for key "%s" is %u bytes)'
445 445 ) % (value, key, lv)
446 446 raise error.ProgrammingError(msg)
447 447 data.append(lk)
448 448 data.append(lv)
449 449 totalsize += lk + lv
450 450 data[0] = totalsize
451 451 data = [_pack(format, *data)]
452 452 for key, value in metadata:
453 453 data.append(key)
454 454 data.append(value)
455 455 return b''.join(data)
456 456
457 457
458 458 def _fm1readmarkers(data, off, stop):
459 459 native = getattr(parsers, 'fm1readmarkers', None)
460 460 if not native:
461 461 return _fm1purereadmarkers(data, off, stop)
462 462 return native(data, off, stop)
463 463
464 464
465 465 # mapping to read/write various marker formats
466 466 # <version> -> (decoder, encoder)
467 467 formats = {
468 468 _fm0version: (_fm0readmarkers, _fm0encodeonemarker),
469 469 _fm1version: (_fm1readmarkers, _fm1encodeonemarker),
470 470 }
471 471
472 472
473 473 def _readmarkerversion(data):
474 474 return _unpack(b'>B', data[0:1])[0]
475 475
476 476
477 477 @util.nogc
478 478 def _readmarkers(data, off=None, stop=None):
479 479 """Read and enumerate markers from raw data"""
480 480 diskversion = _readmarkerversion(data)
481 481 if not off:
482 482 off = 1 # skip 1 byte version number
483 483 if stop is None:
484 484 stop = len(data)
485 485 if diskversion not in formats:
486 486 msg = _(b'parsing obsolete marker: unknown version %r') % diskversion
487 487 raise error.UnknownVersion(msg, version=diskversion)
488 488 return diskversion, formats[diskversion][0](data, off, stop)
489 489
490 490
491 491 def encodeheader(version=_fm0version):
492 492 return _pack(b'>B', version)
493 493
494 494
495 495 def encodemarkers(markers, addheader=False, version=_fm0version):
496 496 # Kept separate from flushmarkers(), it will be reused for
497 497 # markers exchange.
498 498 encodeone = formats[version][1]
499 499 if addheader:
500 500 yield encodeheader(version)
501 501 for marker in markers:
502 502 yield encodeone(marker)
503 503
504 504
505 505 @util.nogc
506 506 def _addsuccessors(successors, markers):
507 507 for mark in markers:
508 508 successors.setdefault(mark[0], set()).add(mark)
509 509
510 510
511 511 @util.nogc
512 512 def _addpredecessors(predecessors, markers):
513 513 for mark in markers:
514 514 for suc in mark[1]:
515 515 predecessors.setdefault(suc, set()).add(mark)
516 516
517 517
518 518 @util.nogc
519 519 def _addchildren(children, markers):
520 520 for mark in markers:
521 521 parents = mark[5]
522 522 if parents is not None:
523 523 for p in parents:
524 524 children.setdefault(p, set()).add(mark)
525 525
526 526
527 527 def _checkinvalidmarkers(repo, markers):
528 528 """search for marker with invalid data and raise error if needed
529 529
530 530 Exist as a separated function to allow the evolve extension for a more
531 531 subtle handling.
532 532 """
533 533 for mark in markers:
534 534 if repo.nullid in mark[1]:
535 535 raise error.Abort(
536 536 _(
537 537 b'bad obsolescence marker detected: '
538 538 b'invalid successors nullid'
539 539 )
540 540 )
541 541
542 542
543 543 class obsstore:
544 544 """Store obsolete markers
545 545
546 546 Markers can be accessed with two mappings:
547 547 - predecessors[x] -> set(markers on predecessors edges of x)
548 548 - successors[x] -> set(markers on successors edges of x)
549 549 - children[x] -> set(markers on predecessors edges of children(x)
550 550 """
551 551
552 552 fields = (b'prec', b'succs', b'flag', b'meta', b'date', b'parents')
553 553 # prec: nodeid, predecessors changesets
554 554 # succs: tuple of nodeid, successor changesets (0-N length)
555 555 # flag: integer, flag field carrying modifier for the markers (see doc)
556 556 # meta: binary blob in UTF-8, encoded metadata dictionary
557 557 # date: (float, int) tuple, date of marker creation
558 558 # parents: (tuple of nodeid) or None, parents of predecessors
559 559 # None is used when no data has been recorded
560 560
561 561 def __init__(self, repo, svfs, defaultformat=_fm1version, readonly=False):
562 562 # caches for various obsolescence related cache
563 563 self.caches = {}
564 564 self.svfs = svfs
565 565 self.repo = repo
566 566 self._defaultformat = defaultformat
567 567 self._readonly = readonly
568 568
569 569 def __iter__(self):
570 570 return iter(self._all)
571 571
572 572 def __len__(self):
573 573 return len(self._all)
574 574
575 575 def __nonzero__(self):
576 576 from . import statichttprepo
577 577
578 578 if isinstance(self.repo, statichttprepo.statichttprepository):
579 579 # If repo is accessed via static HTTP, then we can't use os.stat()
580 580 # to just peek at the file size.
581 581 return len(self._data) > 1
582 582 if not self._cached('_all'):
583 583 try:
584 584 return self.svfs.stat(b'obsstore').st_size > 1
585 585 except OSError as inst:
586 586 if inst.errno != errno.ENOENT:
587 587 raise
588 588 # just build an empty _all list if no obsstore exists, which
589 589 # avoids further stat() syscalls
590 590 return bool(self._all)
591 591
592 592 __bool__ = __nonzero__
593 593
594 594 @property
595 595 def readonly(self):
596 596 """True if marker creation is disabled
597 597
598 598 Remove me in the future when obsolete marker is always on."""
599 599 return self._readonly
600 600
601 601 def create(
602 602 self,
603 603 transaction,
604 604 prec,
605 605 succs=(),
606 606 flag=0,
607 607 parents=None,
608 608 date=None,
609 609 metadata=None,
610 610 ui=None,
611 611 ):
612 612 """obsolete: add a new obsolete marker
613 613
614 614 * ensuring it is hashable
615 615 * check mandatory metadata
616 616 * encode metadata
617 617
618 618 If you are a human writing code creating marker you want to use the
619 619 `createmarkers` function in this module instead.
620 620
621 621 return True if a new marker have been added, False if the markers
622 622 already existed (no op).
623 623 """
624 624 flag = int(flag)
625 625 if metadata is None:
626 626 metadata = {}
627 627 if date is None:
628 628 if b'date' in metadata:
629 629 # as a courtesy for out-of-tree extensions
630 630 date = dateutil.parsedate(metadata.pop(b'date'))
631 631 elif ui is not None:
632 632 date = ui.configdate(b'devel', b'default-date')
633 633 if date is None:
634 634 date = dateutil.makedate()
635 635 else:
636 636 date = dateutil.makedate()
637 637 if flag & usingsha256:
638 638 if len(prec) != 32:
639 639 raise ValueError(prec)
640 640 for succ in succs:
641 641 if len(succ) != 32:
642 642 raise ValueError(succ)
643 643 else:
644 644 if len(prec) != 20:
645 645 raise ValueError(prec)
646 646 for succ in succs:
647 647 if len(succ) != 20:
648 648 raise ValueError(succ)
649 649 if prec in succs:
650 raise ValueError(
651 'in-marker cycle with %s' % pycompat.sysstr(hex(prec))
652 )
650 raise ValueError('in-marker cycle with %s' % prec.hex())
653 651
654 652 metadata = tuple(sorted(metadata.items()))
655 653 for k, v in metadata:
656 654 try:
657 655 # might be better to reject non-ASCII keys
658 656 k.decode('utf-8')
659 657 v.decode('utf-8')
660 658 except UnicodeDecodeError:
661 659 raise error.ProgrammingError(
662 660 b'obsstore metadata must be valid UTF-8 sequence '
663 661 b'(key = %r, value = %r)'
664 662 % (pycompat.bytestr(k), pycompat.bytestr(v))
665 663 )
666 664
667 665 marker = (bytes(prec), tuple(succs), flag, metadata, date, parents)
668 666 return bool(self.add(transaction, [marker]))
669 667
670 668 def add(self, transaction, markers):
671 669 """Add new markers to the store
672 670
673 671 Take care of filtering duplicate.
674 672 Return the number of new marker."""
675 673 if self._readonly:
676 674 raise error.Abort(
677 675 _(b'creating obsolete markers is not enabled on this repo')
678 676 )
679 677 known = set()
680 678 getsuccessors = self.successors.get
681 679 new = []
682 680 for m in markers:
683 681 if m not in getsuccessors(m[0], ()) and m not in known:
684 682 known.add(m)
685 683 new.append(m)
686 684 if new:
687 685 f = self.svfs(b'obsstore', b'ab')
688 686 try:
689 687 offset = f.tell()
690 688 transaction.add(b'obsstore', offset)
691 689 # offset == 0: new file - add the version header
692 690 data = b''.join(encodemarkers(new, offset == 0, self._version))
693 691 f.write(data)
694 692 finally:
695 693 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
696 694 # call 'filecacheentry.refresh()' here
697 695 f.close()
698 696 addedmarkers = transaction.changes.get(b'obsmarkers')
699 697 if addedmarkers is not None:
700 698 addedmarkers.update(new)
701 699 self._addmarkers(new, data)
702 700 # new marker *may* have changed several set. invalidate the cache.
703 701 self.caches.clear()
704 702 # records the number of new markers for the transaction hooks
705 703 previous = int(transaction.hookargs.get(b'new_obsmarkers', b'0'))
706 704 transaction.hookargs[b'new_obsmarkers'] = b'%d' % (previous + len(new))
707 705 return len(new)
708 706
709 707 def mergemarkers(self, transaction, data):
710 708 """merge a binary stream of markers inside the obsstore
711 709
712 710 Returns the number of new markers added."""
713 711 version, markers = _readmarkers(data)
714 712 return self.add(transaction, markers)
715 713
716 714 @propertycache
717 715 def _data(self):
718 716 return self.svfs.tryread(b'obsstore')
719 717
720 718 @propertycache
721 719 def _version(self):
722 720 if len(self._data) >= 1:
723 721 return _readmarkerversion(self._data)
724 722 else:
725 723 return self._defaultformat
726 724
727 725 @propertycache
728 726 def _all(self):
729 727 data = self._data
730 728 if not data:
731 729 return []
732 730 self._version, markers = _readmarkers(data)
733 731 markers = list(markers)
734 732 _checkinvalidmarkers(self.repo, markers)
735 733 return markers
736 734
737 735 @propertycache
738 736 def successors(self):
739 737 successors = {}
740 738 _addsuccessors(successors, self._all)
741 739 return successors
742 740
743 741 @propertycache
744 742 def predecessors(self):
745 743 predecessors = {}
746 744 _addpredecessors(predecessors, self._all)
747 745 return predecessors
748 746
749 747 @propertycache
750 748 def children(self):
751 749 children = {}
752 750 _addchildren(children, self._all)
753 751 return children
754 752
755 753 def _cached(self, attr):
756 754 return attr in self.__dict__
757 755
758 756 def _addmarkers(self, markers, rawdata):
759 757 markers = list(markers) # to allow repeated iteration
760 758 self._data = self._data + rawdata
761 759 self._all.extend(markers)
762 760 if self._cached('successors'):
763 761 _addsuccessors(self.successors, markers)
764 762 if self._cached('predecessors'):
765 763 _addpredecessors(self.predecessors, markers)
766 764 if self._cached('children'):
767 765 _addchildren(self.children, markers)
768 766 _checkinvalidmarkers(self.repo, markers)
769 767
770 768 def relevantmarkers(self, nodes):
771 769 """return a set of all obsolescence markers relevant to a set of nodes.
772 770
773 771 "relevant" to a set of nodes mean:
774 772
775 773 - marker that use this changeset as successor
776 774 - prune marker of direct children on this changeset
777 775 - recursive application of the two rules on predecessors of these
778 776 markers
779 777
780 778 It is a set so you cannot rely on order."""
781 779
782 780 pendingnodes = set(nodes)
783 781 seenmarkers = set()
784 782 seennodes = set(pendingnodes)
785 783 precursorsmarkers = self.predecessors
786 784 succsmarkers = self.successors
787 785 children = self.children
788 786 while pendingnodes:
789 787 direct = set()
790 788 for current in pendingnodes:
791 789 direct.update(precursorsmarkers.get(current, ()))
792 790 pruned = [m for m in children.get(current, ()) if not m[1]]
793 791 direct.update(pruned)
794 792 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
795 793 direct.update(pruned)
796 794 direct -= seenmarkers
797 795 pendingnodes = {m[0] for m in direct}
798 796 seenmarkers |= direct
799 797 pendingnodes -= seennodes
800 798 seennodes |= pendingnodes
801 799 return seenmarkers
802 800
803 801
804 802 def makestore(ui, repo):
805 803 """Create an obsstore instance from a repo."""
806 804 # read default format for new obsstore.
807 805 # developer config: format.obsstore-version
808 806 defaultformat = ui.configint(b'format', b'obsstore-version')
809 807 # rely on obsstore class default when possible.
810 808 kwargs = {}
811 809 if defaultformat is not None:
812 810 kwargs['defaultformat'] = defaultformat
813 811 readonly = not isenabled(repo, createmarkersopt)
814 812 store = obsstore(repo, repo.svfs, readonly=readonly, **kwargs)
815 813 if store and readonly:
816 814 ui.warn(
817 815 _(b'obsolete feature not enabled but %i markers found!\n')
818 816 % len(list(store))
819 817 )
820 818 return store
821 819
822 820
823 821 def commonversion(versions):
824 822 """Return the newest version listed in both versions and our local formats.
825 823
826 824 Returns None if no common version exists.
827 825 """
828 826 versions.sort(reverse=True)
829 827 # search for highest version known on both side
830 828 for v in versions:
831 829 if v in formats:
832 830 return v
833 831 return None
834 832
835 833
836 834 # arbitrary picked to fit into 8K limit from HTTP server
837 835 # you have to take in account:
838 836 # - the version header
839 837 # - the base85 encoding
840 838 _maxpayload = 5300
841 839
842 840
843 841 def _pushkeyescape(markers):
844 842 """encode markers into a dict suitable for pushkey exchange
845 843
846 844 - binary data is base85 encoded
847 845 - split in chunks smaller than 5300 bytes"""
848 846 keys = {}
849 847 parts = []
850 848 currentlen = _maxpayload * 2 # ensure we create a new part
851 849 for marker in markers:
852 850 nextdata = _fm0encodeonemarker(marker)
853 851 if len(nextdata) + currentlen > _maxpayload:
854 852 currentpart = []
855 853 currentlen = 0
856 854 parts.append(currentpart)
857 855 currentpart.append(nextdata)
858 856 currentlen += len(nextdata)
859 857 for idx, part in enumerate(reversed(parts)):
860 858 data = b''.join([_pack(b'>B', _fm0version)] + part)
861 859 keys[b'dump%i' % idx] = util.b85encode(data)
862 860 return keys
863 861
864 862
865 863 def listmarkers(repo):
866 864 """List markers over pushkey"""
867 865 if not repo.obsstore:
868 866 return {}
869 867 return _pushkeyescape(sorted(repo.obsstore))
870 868
871 869
872 870 def pushmarker(repo, key, old, new):
873 871 """Push markers over pushkey"""
874 872 if not key.startswith(b'dump'):
875 873 repo.ui.warn(_(b'unknown key: %r') % key)
876 874 return False
877 875 if old:
878 876 repo.ui.warn(_(b'unexpected old value for %r') % key)
879 877 return False
880 878 data = util.b85decode(new)
881 879 with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr:
882 880 repo.obsstore.mergemarkers(tr, data)
883 881 repo.invalidatevolatilesets()
884 882 return True
885 883
886 884
887 885 # mapping of 'set-name' -> <function to compute this set>
888 886 cachefuncs = {}
889 887
890 888
891 889 def cachefor(name):
892 890 """Decorator to register a function as computing the cache for a set"""
893 891
894 892 def decorator(func):
895 893 if name in cachefuncs:
896 894 msg = b"duplicated registration for volatileset '%s' (existing: %r)"
897 895 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
898 896 cachefuncs[name] = func
899 897 return func
900 898
901 899 return decorator
902 900
903 901
904 902 def getrevs(repo, name):
905 903 """Return the set of revision that belong to the <name> set
906 904
907 905 Such access may compute the set and cache it for future use"""
908 906 repo = repo.unfiltered()
909 907 with util.timedcm('getrevs %s', name):
910 908 if not repo.obsstore:
911 909 return frozenset()
912 910 if name not in repo.obsstore.caches:
913 911 repo.obsstore.caches[name] = cachefuncs[name](repo)
914 912 return repo.obsstore.caches[name]
915 913
916 914
917 915 # To be simple we need to invalidate obsolescence cache when:
918 916 #
919 917 # - new changeset is added:
920 918 # - public phase is changed
921 919 # - obsolescence marker are added
922 920 # - strip is used a repo
923 921 def clearobscaches(repo):
924 922 """Remove all obsolescence related cache from a repo
925 923
926 924 This remove all cache in obsstore is the obsstore already exist on the
927 925 repo.
928 926
929 927 (We could be smarter here given the exact event that trigger the cache
930 928 clearing)"""
931 929 # only clear cache is there is obsstore data in this repo
932 930 if b'obsstore' in repo._filecache:
933 931 repo.obsstore.caches.clear()
934 932
935 933
936 934 def _mutablerevs(repo):
937 935 """the set of mutable revision in the repository"""
938 936 return repo._phasecache.getrevset(repo, phases.mutablephases)
939 937
940 938
941 939 @cachefor(b'obsolete')
942 940 def _computeobsoleteset(repo):
943 941 """the set of obsolete revisions"""
944 942 getnode = repo.changelog.node
945 943 notpublic = _mutablerevs(repo)
946 944 isobs = repo.obsstore.successors.__contains__
947 945 return frozenset(r for r in notpublic if isobs(getnode(r)))
948 946
949 947
950 948 @cachefor(b'orphan')
951 949 def _computeorphanset(repo):
952 950 """the set of non obsolete revisions with obsolete parents"""
953 951 pfunc = repo.changelog.parentrevs
954 952 mutable = _mutablerevs(repo)
955 953 obsolete = getrevs(repo, b'obsolete')
956 954 others = mutable - obsolete
957 955 unstable = set()
958 956 for r in sorted(others):
959 957 # A rev is unstable if one of its parent is obsolete or unstable
960 958 # this works since we traverse following growing rev order
961 959 for p in pfunc(r):
962 960 if p in obsolete or p in unstable:
963 961 unstable.add(r)
964 962 break
965 963 return frozenset(unstable)
966 964
967 965
968 966 @cachefor(b'suspended')
969 967 def _computesuspendedset(repo):
970 968 """the set of obsolete parents with non obsolete descendants"""
971 969 suspended = repo.changelog.ancestors(getrevs(repo, b'orphan'))
972 970 return frozenset(r for r in getrevs(repo, b'obsolete') if r in suspended)
973 971
974 972
975 973 @cachefor(b'extinct')
976 974 def _computeextinctset(repo):
977 975 """the set of obsolete parents without non obsolete descendants"""
978 976 return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended')
979 977
980 978
981 979 @cachefor(b'phasedivergent')
982 980 def _computephasedivergentset(repo):
983 981 """the set of revs trying to obsolete public revisions"""
984 982 bumped = set()
985 983 # util function (avoid attribute lookup in the loop)
986 984 phase = repo._phasecache.phase # would be faster to grab the full list
987 985 public = phases.public
988 986 cl = repo.changelog
989 987 torev = cl.index.get_rev
990 988 tonode = cl.node
991 989 obsstore = repo.obsstore
992 990 for rev in repo.revs(b'(not public()) and (not obsolete())'):
993 991 # We only evaluate mutable, non-obsolete revision
994 992 node = tonode(rev)
995 993 # (future) A cache of predecessors may worth if split is very common
996 994 for pnode in obsutil.allpredecessors(
997 995 obsstore, [node], ignoreflags=bumpedfix
998 996 ):
999 997 prev = torev(pnode) # unfiltered! but so is phasecache
1000 998 if (prev is not None) and (phase(repo, prev) <= public):
1001 999 # we have a public predecessor
1002 1000 bumped.add(rev)
1003 1001 break # Next draft!
1004 1002 return frozenset(bumped)
1005 1003
1006 1004
1007 1005 @cachefor(b'contentdivergent')
1008 1006 def _computecontentdivergentset(repo):
1009 1007 """the set of rev that compete to be the final successors of some revision."""
1010 1008 divergent = set()
1011 1009 obsstore = repo.obsstore
1012 1010 newermap = {}
1013 1011 tonode = repo.changelog.node
1014 1012 for rev in repo.revs(b'(not public()) - obsolete()'):
1015 1013 node = tonode(rev)
1016 1014 mark = obsstore.predecessors.get(node, ())
1017 1015 toprocess = set(mark)
1018 1016 seen = set()
1019 1017 while toprocess:
1020 1018 prec = toprocess.pop()[0]
1021 1019 if prec in seen:
1022 1020 continue # emergency cycle hanging prevention
1023 1021 seen.add(prec)
1024 1022 if prec not in newermap:
1025 1023 obsutil.successorssets(repo, prec, cache=newermap)
1026 1024 newer = [n for n in newermap[prec] if n]
1027 1025 if len(newer) > 1:
1028 1026 divergent.add(rev)
1029 1027 break
1030 1028 toprocess.update(obsstore.predecessors.get(prec, ()))
1031 1029 return frozenset(divergent)
1032 1030
1033 1031
1034 1032 def makefoldid(relation, user):
1035 1033
1036 1034 folddigest = hashutil.sha1(user)
1037 1035 for p in relation[0] + relation[1]:
1038 1036 folddigest.update(b'%d' % p.rev())
1039 1037 folddigest.update(p.node())
1040 1038 # Since fold only has to compete against fold for the same successors, it
1041 1039 # seems fine to use a small ID. Smaller ID save space.
1042 1040 return hex(folddigest.digest())[:8]
1043 1041
1044 1042
1045 1043 def createmarkers(
1046 1044 repo, relations, flag=0, date=None, metadata=None, operation=None
1047 1045 ):
1048 1046 """Add obsolete markers between changesets in a repo
1049 1047
1050 1048 <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}])
1051 1049 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1052 1050 containing metadata for this marker only. It is merged with the global
1053 1051 metadata specified through the `metadata` argument of this function.
1054 1052 Any string values in metadata must be UTF-8 bytes.
1055 1053
1056 1054 Trying to obsolete a public changeset will raise an exception.
1057 1055
1058 1056 Current user and date are used except if specified otherwise in the
1059 1057 metadata attribute.
1060 1058
1061 1059 This function operates within a transaction of its own, but does
1062 1060 not take any lock on the repo.
1063 1061 """
1064 1062 # prepare metadata
1065 1063 if metadata is None:
1066 1064 metadata = {}
1067 1065 if b'user' not in metadata:
1068 1066 luser = (
1069 1067 repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username()
1070 1068 )
1071 1069 metadata[b'user'] = encoding.fromlocal(luser)
1072 1070
1073 1071 # Operation metadata handling
1074 1072 useoperation = repo.ui.configbool(
1075 1073 b'experimental', b'evolution.track-operation'
1076 1074 )
1077 1075 if useoperation and operation:
1078 1076 metadata[b'operation'] = operation
1079 1077
1080 1078 # Effect flag metadata handling
1081 1079 saveeffectflag = repo.ui.configbool(
1082 1080 b'experimental', b'evolution.effect-flags'
1083 1081 )
1084 1082
1085 1083 with repo.transaction(b'add-obsolescence-marker') as tr:
1086 1084 markerargs = []
1087 1085 for rel in relations:
1088 1086 predecessors = rel[0]
1089 1087 if not isinstance(predecessors, tuple):
1090 1088 # preserve compat with old API until all caller are migrated
1091 1089 predecessors = (predecessors,)
1092 1090 if len(predecessors) > 1 and len(rel[1]) != 1:
1093 1091 msg = b'Fold markers can only have 1 successors, not %d'
1094 1092 raise error.ProgrammingError(msg % len(rel[1]))
1095 1093 foldid = None
1096 1094 foldsize = len(predecessors)
1097 1095 if 1 < foldsize:
1098 1096 foldid = makefoldid(rel, metadata[b'user'])
1099 1097 for foldidx, prec in enumerate(predecessors, 1):
1100 1098 sucs = rel[1]
1101 1099 localmetadata = metadata.copy()
1102 1100 if len(rel) > 2:
1103 1101 localmetadata.update(rel[2])
1104 1102 if foldid is not None:
1105 1103 localmetadata[b'fold-id'] = foldid
1106 1104 localmetadata[b'fold-idx'] = b'%d' % foldidx
1107 1105 localmetadata[b'fold-size'] = b'%d' % foldsize
1108 1106
1109 1107 if not prec.mutable():
1110 1108 raise error.Abort(
1111 1109 _(b"cannot obsolete public changeset: %s") % prec,
1112 1110 hint=b"see 'hg help phases' for details",
1113 1111 )
1114 1112 nprec = prec.node()
1115 1113 nsucs = tuple(s.node() for s in sucs)
1116 1114 npare = None
1117 1115 if not nsucs:
1118 1116 npare = tuple(p.node() for p in prec.parents())
1119 1117 if nprec in nsucs:
1120 1118 raise error.Abort(
1121 1119 _(b"changeset %s cannot obsolete itself") % prec
1122 1120 )
1123 1121
1124 1122 # Effect flag can be different by relation
1125 1123 if saveeffectflag:
1126 1124 # The effect flag is saved in a versioned field name for
1127 1125 # future evolution
1128 1126 effectflag = obsutil.geteffectflag(prec, sucs)
1129 1127 localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag
1130 1128
1131 1129 # Creating the marker causes the hidden cache to become
1132 1130 # invalid, which causes recomputation when we ask for
1133 1131 # prec.parents() above. Resulting in n^2 behavior. So let's
1134 1132 # prepare all of the args first, then create the markers.
1135 1133 markerargs.append((nprec, nsucs, npare, localmetadata))
1136 1134
1137 1135 for args in markerargs:
1138 1136 nprec, nsucs, npare, localmetadata = args
1139 1137 repo.obsstore.create(
1140 1138 tr,
1141 1139 nprec,
1142 1140 nsucs,
1143 1141 flag,
1144 1142 parents=npare,
1145 1143 date=date,
1146 1144 metadata=localmetadata,
1147 1145 ui=repo.ui,
1148 1146 )
1149 1147 repo.filteredrevcache.clear()
General Comments 0
You need to be logged in to leave comments. Login now