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