1# repoview.py - Filtered view of a localrepo object 2# 3# Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org> 4# Logilab SA <contact@logilab.fr> 5# 6# This software may be used and distributed according to the terms of the 7# GNU General Public License version 2 or any later version. 8 9from __future__ import absolute_import 10 11import copy 12import weakref 13 14from .i18n import _ 15from .node import ( 16 hex, 17 nullrev, 18) 19from .pycompat import ( 20 delattr, 21 getattr, 22 setattr, 23) 24from . import ( 25 error, 26 obsolete, 27 phases, 28 pycompat, 29 tags as tagsmod, 30 util, 31) 32from .utils import repoviewutil 33 34 35def hideablerevs(repo): 36 """Revision candidates to be hidden 37 38 This is a standalone function to allow extensions to wrap it. 39 40 Because we use the set of immutable changesets as a fallback subset in 41 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set 42 "public" changesets as "hideable". Doing so would break multiple code 43 assertions and lead to crashes.""" 44 obsoletes = obsolete.getrevs(repo, b'obsolete') 45 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases) 46 internals = frozenset(internals) 47 return obsoletes | internals 48 49 50def pinnedrevs(repo): 51 """revisions blocking hidden changesets from being filtered""" 52 53 cl = repo.changelog 54 pinned = set() 55 pinned.update([par.rev() for par in repo[None].parents()]) 56 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()]) 57 58 tags = {} 59 tagsmod.readlocaltags(repo.ui, repo, tags, {}) 60 if tags: 61 rev = cl.index.get_rev 62 pinned.update(rev(t[0]) for t in tags.values()) 63 pinned.discard(None) 64 65 # Avoid cycle: mercurial.filemerge -> mercurial.templater -> 66 # mercurial.templatefuncs -> mercurial.revset -> mercurial.repoview -> 67 # mercurial.mergestate -> mercurial.filemerge 68 from . import mergestate 69 70 ms = mergestate.mergestate.read(repo) 71 if ms.active() and ms.unresolvedcount(): 72 for node in (ms.local, ms.other): 73 rev = cl.index.get_rev(node) 74 if rev is not None: 75 pinned.add(rev) 76 77 return pinned 78 79 80def _revealancestors(pfunc, hidden, revs): 81 """reveals contiguous chains of hidden ancestors of 'revs' by removing them 82 from 'hidden' 83 84 - pfunc(r): a funtion returning parent of 'r', 85 - hidden: the (preliminary) hidden revisions, to be updated 86 - revs: iterable of revnum, 87 88 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are 89 *not* revealed) 90 """ 91 stack = list(revs) 92 while stack: 93 for p in pfunc(stack.pop()): 94 if p != nullrev and p in hidden: 95 hidden.remove(p) 96 stack.append(p) 97 98 99def computehidden(repo, visibilityexceptions=None): 100 """compute the set of hidden revision to filter 101 102 During most operation hidden should be filtered.""" 103 assert not repo.changelog.filteredrevs 104 105 hidden = hideablerevs(repo) 106 if hidden: 107 hidden = set(hidden - pinnedrevs(repo)) 108 if visibilityexceptions: 109 hidden -= visibilityexceptions 110 pfunc = repo.changelog.parentrevs 111 mutable = repo._phasecache.getrevset(repo, phases.mutablephases) 112 113 visible = mutable - hidden 114 _revealancestors(pfunc, hidden, visible) 115 return frozenset(hidden) 116 117 118def computesecret(repo, visibilityexceptions=None): 119 """compute the set of revision that can never be exposed through hgweb 120 121 Changeset in the secret phase (or above) should stay unaccessible.""" 122 assert not repo.changelog.filteredrevs 123 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases) 124 return frozenset(secrets) 125 126 127def computeunserved(repo, visibilityexceptions=None): 128 """compute the set of revision that should be filtered when used a server 129 130 Secret and hidden changeset should not pretend to be here.""" 131 assert not repo.changelog.filteredrevs 132 # fast path in simple case to avoid impact of non optimised code 133 hiddens = filterrevs(repo, b'visible') 134 secrets = filterrevs(repo, b'served.hidden') 135 if secrets: 136 return frozenset(hiddens | secrets) 137 else: 138 return hiddens 139 140 141def computemutable(repo, visibilityexceptions=None): 142 assert not repo.changelog.filteredrevs 143 # fast check to avoid revset call on huge repo 144 if repo._phasecache.hasnonpublicphases(repo): 145 return frozenset(repo._phasecache.getrevset(repo, phases.mutablephases)) 146 return frozenset() 147 148 149def computeimpactable(repo, visibilityexceptions=None): 150 """Everything impactable by mutable revision 151 152 The immutable filter still have some chance to get invalidated. This will 153 happen when: 154 155 - you garbage collect hidden changeset, 156 - public phase is moved backward, 157 - something is changed in the filtering (this could be fixed) 158 159 This filter out any mutable changeset and any public changeset that may be 160 impacted by something happening to a mutable revision. 161 162 This is achieved by filtered everything with a revision number equal or 163 higher than the first mutable changeset is filtered.""" 164 assert not repo.changelog.filteredrevs 165 cl = repo.changelog 166 firstmutable = len(cl) 167 roots = repo._phasecache.nonpublicphaseroots(repo) 168 if roots: 169 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots)) 170 # protect from nullrev root 171 firstmutable = max(0, firstmutable) 172 return frozenset(pycompat.xrange(firstmutable, len(cl))) 173 174 175# function to compute filtered set 176# 177# When adding a new filter you MUST update the table at: 178# mercurial.utils.repoviewutil.subsettable 179# Otherwise your filter will have to recompute all its branches cache 180# from scratch (very slow). 181filtertable = { 182 b'visible': computehidden, 183 b'visible-hidden': computehidden, 184 b'served.hidden': computesecret, 185 b'served': computeunserved, 186 b'immutable': computemutable, 187 b'base': computeimpactable, 188} 189 190# set of filter level that will include the working copy parent no matter what. 191filter_has_wc = {b'visible', b'visible-hidden'} 192 193_basefiltername = list(filtertable) 194 195 196def extrafilter(ui): 197 """initialize extra filter and return its id 198 199 If extra filtering is configured, we make sure the associated filtered view 200 are declared and return the associated id. 201 """ 202 frevs = ui.config(b'experimental', b'extra-filter-revs') 203 if frevs is None: 204 return None 205 206 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12] 207 208 combine = lambda fname: fname + b'%' + fid 209 210 subsettable = repoviewutil.subsettable 211 212 if combine(b'base') not in filtertable: 213 for name in _basefiltername: 214 215 def extrafilteredrevs(repo, *args, **kwargs): 216 baserevs = filtertable[name](repo, *args, **kwargs) 217 extrarevs = frozenset(repo.revs(frevs)) 218 return baserevs | extrarevs 219 220 filtertable[combine(name)] = extrafilteredrevs 221 if name in subsettable: 222 subsettable[combine(name)] = combine(subsettable[name]) 223 return fid 224 225 226def filterrevs(repo, filtername, visibilityexceptions=None): 227 """returns set of filtered revision for this filter name 228 229 visibilityexceptions is a set of revs which must are exceptions for 230 hidden-state and must be visible. They are dynamic and hence we should not 231 cache it's result""" 232 if filtername not in repo.filteredrevcache: 233 if repo.ui.configbool(b'devel', b'debug.repo-filters'): 234 msg = b'computing revision filter for "%s"' 235 msg %= filtername 236 if repo.ui.tracebackflag and repo.ui.debugflag: 237 # XXX use ui.write_err 238 util.debugstacktrace( 239 msg, 240 f=repo.ui._fout, 241 otherf=repo.ui._ferr, 242 prefix=b'debug.filters: ', 243 ) 244 else: 245 repo.ui.debug(b'debug.filters: %s\n' % msg) 246 func = filtertable[filtername] 247 if visibilityexceptions: 248 return func(repo.unfiltered, visibilityexceptions) 249 repo.filteredrevcache[filtername] = func(repo.unfiltered()) 250 return repo.filteredrevcache[filtername] 251 252 253def wrapchangelog(unfichangelog, filteredrevs): 254 cl = copy.copy(unfichangelog) 255 cl.filteredrevs = filteredrevs 256 257 class filteredchangelog(filteredchangelogmixin, cl.__class__): 258 pass 259 260 cl.__class__ = filteredchangelog 261 262 return cl 263 264 265class filteredchangelogmixin(object): 266 def tiprev(self): 267 """filtered version of revlog.tiprev""" 268 for i in pycompat.xrange(len(self) - 1, -2, -1): 269 if i not in self.filteredrevs: 270 return i 271 272 def __contains__(self, rev): 273 """filtered version of revlog.__contains__""" 274 return 0 <= rev < len(self) and rev not in self.filteredrevs 275 276 def __iter__(self): 277 """filtered version of revlog.__iter__""" 278 279 def filterediter(): 280 for i in pycompat.xrange(len(self)): 281 if i not in self.filteredrevs: 282 yield i 283 284 return filterediter() 285 286 def revs(self, start=0, stop=None): 287 """filtered version of revlog.revs""" 288 for i in super(filteredchangelogmixin, self).revs(start, stop): 289 if i not in self.filteredrevs: 290 yield i 291 292 def _checknofilteredinrevs(self, revs): 293 """raise the appropriate error if 'revs' contains a filtered revision 294 295 This returns a version of 'revs' to be used thereafter by the caller. 296 In particular, if revs is an iterator, it is converted into a set. 297 """ 298 safehasattr = util.safehasattr 299 if safehasattr(revs, '__next__'): 300 # Note that inspect.isgenerator() is not true for iterators, 301 revs = set(revs) 302 303 filteredrevs = self.filteredrevs 304 if safehasattr(revs, 'first'): # smartset 305 offenders = revs & filteredrevs 306 else: 307 offenders = filteredrevs.intersection(revs) 308 309 for rev in offenders: 310 raise error.FilteredIndexError(rev) 311 return revs 312 313 def headrevs(self, revs=None): 314 if revs is None: 315 try: 316 return self.index.headrevsfiltered(self.filteredrevs) 317 # AttributeError covers non-c-extension environments and 318 # old c extensions without filter handling. 319 except AttributeError: 320 return self._headrevs() 321 322 revs = self._checknofilteredinrevs(revs) 323 return super(filteredchangelogmixin, self).headrevs(revs) 324 325 def strip(self, *args, **kwargs): 326 # XXX make something better than assert 327 # We can't expect proper strip behavior if we are filtered. 328 assert not self.filteredrevs 329 super(filteredchangelogmixin, self).strip(*args, **kwargs) 330 331 def rev(self, node): 332 """filtered version of revlog.rev""" 333 r = super(filteredchangelogmixin, self).rev(node) 334 if r in self.filteredrevs: 335 raise error.FilteredLookupError( 336 hex(node), self.display_id, _(b'filtered node') 337 ) 338 return r 339 340 def node(self, rev): 341 """filtered version of revlog.node""" 342 if rev in self.filteredrevs: 343 raise error.FilteredIndexError(rev) 344 return super(filteredchangelogmixin, self).node(rev) 345 346 def linkrev(self, rev): 347 """filtered version of revlog.linkrev""" 348 if rev in self.filteredrevs: 349 raise error.FilteredIndexError(rev) 350 return super(filteredchangelogmixin, self).linkrev(rev) 351 352 def parentrevs(self, rev): 353 """filtered version of revlog.parentrevs""" 354 if rev in self.filteredrevs: 355 raise error.FilteredIndexError(rev) 356 return super(filteredchangelogmixin, self).parentrevs(rev) 357 358 def flags(self, rev): 359 """filtered version of revlog.flags""" 360 if rev in self.filteredrevs: 361 raise error.FilteredIndexError(rev) 362 return super(filteredchangelogmixin, self).flags(rev) 363 364 365class repoview(object): 366 """Provide a read/write view of a repo through a filtered changelog 367 368 This object is used to access a filtered version of a repository without 369 altering the original repository object itself. We can not alter the 370 original object for two main reasons: 371 - It prevents the use of a repo with multiple filters at the same time. In 372 particular when multiple threads are involved. 373 - It makes scope of the filtering harder to control. 374 375 This object behaves very closely to the original repository. All attribute 376 operations are done on the original repository: 377 - An access to `repoview.someattr` actually returns `repo.someattr`, 378 - A write to `repoview.someattr` actually sets value of `repo.someattr`, 379 - A deletion of `repoview.someattr` actually drops `someattr` 380 from `repo.__dict__`. 381 382 The only exception is the `changelog` property. It is overridden to return 383 a (surface) copy of `repo.changelog` with some revisions filtered. The 384 `filtername` attribute of the view control the revisions that need to be 385 filtered. (the fact the changelog is copied is an implementation detail). 386 387 Unlike attributes, this object intercepts all method calls. This means that 388 all methods are run on the `repoview` object with the filtered `changelog` 389 property. For this purpose the simple `repoview` class must be mixed with 390 the actual class of the repository. This ensures that the resulting 391 `repoview` object have the very same methods than the repo object. This 392 leads to the property below. 393 394 repoview.method() --> repo.__class__.method(repoview) 395 396 The inheritance has to be done dynamically because `repo` can be of any 397 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`. 398 """ 399 400 def __init__(self, repo, filtername, visibilityexceptions=None): 401 object.__setattr__(self, '_unfilteredrepo', repo) 402 object.__setattr__(self, 'filtername', filtername) 403 object.__setattr__(self, '_clcachekey', None) 404 object.__setattr__(self, '_clcache', None) 405 # revs which are exceptions and must not be hidden 406 object.__setattr__(self, '_visibilityexceptions', visibilityexceptions) 407 408 # not a propertycache on purpose we shall implement a proper cache later 409 @property 410 def changelog(self): 411 """return a filtered version of the changeset 412 413 this changelog must not be used for writing""" 414 # some cache may be implemented later 415 unfi = self._unfilteredrepo 416 unfichangelog = unfi.changelog 417 # bypass call to changelog.method 418 unfiindex = unfichangelog.index 419 unfilen = len(unfiindex) 420 unfinode = unfiindex[unfilen - 1][7] 421 with util.timedcm('repo filter for %s', self.filtername): 422 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions) 423 cl = self._clcache 424 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed) 425 # if cl.index is not unfiindex, unfi.changelog would be 426 # recreated, and our clcache refers to garbage object 427 if cl is not None and ( 428 cl.index is not unfiindex or newkey != self._clcachekey 429 ): 430 cl = None 431 # could have been made None by the previous if 432 if cl is None: 433 # Only filter if there's something to filter 434 cl = wrapchangelog(unfichangelog, revs) if revs else unfichangelog 435 object.__setattr__(self, '_clcache', cl) 436 object.__setattr__(self, '_clcachekey', newkey) 437 return cl 438 439 def unfiltered(self): 440 """Return an unfiltered version of a repo""" 441 return self._unfilteredrepo 442 443 def filtered(self, name, visibilityexceptions=None): 444 """Return a filtered version of a repository""" 445 if name == self.filtername and not visibilityexceptions: 446 return self 447 return self.unfiltered().filtered(name, visibilityexceptions) 448 449 def __repr__(self): 450 return '<%s:%s %r>' % ( 451 self.__class__.__name__, 452 pycompat.sysstr(self.filtername), 453 self.unfiltered(), 454 ) 455 456 # everything access are forwarded to the proxied repo 457 def __getattr__(self, attr): 458 return getattr(self._unfilteredrepo, attr) 459 460 def __setattr__(self, attr, value): 461 return setattr(self._unfilteredrepo, attr, value) 462 463 def __delattr__(self, attr): 464 return delattr(self._unfilteredrepo, attr) 465 466 467# Dynamically created classes introduce memory cycles via __mro__. See 468# https://bugs.python.org/issue17950. 469# This need of the garbage collector can turn into memory leak in 470# Python <3.4, which is the first version released with PEP 442. 471_filteredrepotypes = weakref.WeakKeyDictionary() 472 473 474def newtype(base): 475 """Create a new type with the repoview mixin and the given base class""" 476 ref = _filteredrepotypes.get(base) 477 if ref is not None: 478 cls = ref() 479 if cls is not None: 480 return cls 481 482 class filteredrepo(repoview, base): 483 pass 484 485 _filteredrepotypes[base] = weakref.ref(filteredrepo) 486 # do not reread from weakref to be 100% sure not to return None 487 return filteredrepo 488