1"""enable a minimal verison of topic for server
2
3! This extensions is not actively maintained
4! We recommand using the main topic extension instead
5
6Non publishing repository will see topic as "branch:topic" in the branch field.
7
8In addition to adding the extensions, the feature must be manually enabled in the config:
9
10    [experimental]
11    server-mini-topic = yes
12"""
13import hashlib
14import contextlib
15
16from mercurial import (
17    branchmap,
18    context,
19    encoding,
20    extensions,
21    node,
22    registrar,
23    util,
24)
25
26from mercurial import wireprotov1server
27
28if util.safehasattr(registrar, 'configitem'):
29
30    configtable = {}
31    configitem = registrar.configitem(configtable)
32    configitem(b'experimental', b'server-mini-topic',
33               default=False,
34    )
35
36# hg <= 5.4 (e2d17974a869)
37def nonpublicphaseroots(repo):
38    if util.safehasattr(repo._phasecache, 'nonpublicphaseroots'):
39        return repo._phasecache.nonpublicphaseroots(repo)
40    return set().union(
41        *[roots for roots in repo._phasecache.phaseroots[1:] if roots]
42    )
43
44def hasminitopic(repo):
45    """true if minitopic is enabled on the repository
46
47    (The value is cached on the repository)
48    """
49    enabled = getattr(repo, '_hasminitopic', None)
50    if enabled is None:
51        enabled = (repo.ui.configbool(b'experimental', b'server-mini-topic')
52                   and not repo.publishing())
53        repo._hasminitopic = enabled
54    return enabled
55
56### make topic visible though "ctx.branch()"
57
58def topicbranch(orig, self):
59    branch = orig(self)
60    if hasminitopic(self._repo) and self.phase():
61        topic = self._changeset.extra.get(b'topic')
62        if topic is not None:
63            topic = encoding.tolocal(topic)
64            branch = b'%s:%s' % (branch, topic)
65    return branch
66
67### avoid caching topic data in rev-branch-cache
68
69class revbranchcacheoverlay(object):
70    """revbranch mixin that don't use the cache for non public changeset"""
71
72    def _init__(self, *args, **kwargs):
73        super(revbranchcacheoverlay, self).__init__(*args, **kwargs)
74        if r'branchinfo' in vars(self):
75            del self.branchinfo
76
77    def branchinfo(self, rev, changelog=None):
78        """return branch name and close flag for rev, using and updating
79        persistent cache."""
80        phase = self._repo._phasecache.phase(self._repo, rev)
81        if phase:
82            ctx = self._repo[rev]
83            return ctx.branch(), ctx.closesbranch()
84        return super(revbranchcacheoverlay, self).branchinfo(rev)
85
86def reposetup(ui, repo):
87    """install a repo class with a special revbranchcache"""
88
89    if hasminitopic(repo):
90        repo = repo.unfiltered()
91
92        class minitopicrepo(repo.__class__):
93            """repository subclass that install the modified cache"""
94
95            def revbranchcache(self):
96                if self._revbranchcache is None:
97                    cache = super(minitopicrepo, self).revbranchcache()
98
99                    class topicawarerbc(revbranchcacheoverlay, cache.__class__):
100                        pass
101                    cache.__class__ = topicawarerbc
102                    if r'branchinfo' in vars(cache):
103                        del cache.branchinfo
104                    self._revbranchcache = cache
105                return self._revbranchcache
106
107        repo.__class__ = minitopicrepo
108
109### topic aware branch head cache
110
111def _phaseshash(repo, maxrev):
112    """uniq ID for a phase matching a set of rev"""
113    revs = set()
114    cl = repo.changelog
115    fr = cl.filteredrevs
116    nm = cl.nodemap
117    for n in nonpublicphaseroots(repo):
118        r = nm.get(n)
119        if r not in fr and r < maxrev:
120            revs.add(r)
121    key = node.nullid
122    revs = sorted(revs)
123    if revs:
124        s = hashlib.sha1()
125        for rev in revs:
126            s.update(b'%d;' % rev)
127        key = s.digest()
128    return key
129
130# needed to prevent reference used for 'super()' call using in branchmap.py to
131# no go into cycle. (yes, URG)
132_oldbranchmap = branchmap.branchcache
133
134@contextlib.contextmanager
135def oldbranchmap():
136    previous = branchmap.branchcache
137    try:
138        branchmap.branchcache = _oldbranchmap
139        yield
140    finally:
141        branchmap.branchcache = previous
142
143_publiconly = set([
144    b'base',
145    b'immutable',
146])
147
148def mighttopic(repo):
149    return hasminitopic(repo) and repo.filtername not in _publiconly
150
151class _topiccache(branchmap.branchcache): # combine me with branchmap.branchcache
152    @classmethod
153    def fromfile(cls, repo):
154        orig = super(_topiccache, cls).fromfile
155        return wrapread(orig, repo)
156
157    def __init__(self, *args, **kwargs):
158        # super() call may fail otherwise
159        with oldbranchmap():
160            super(_topiccache, self).__init__(*args, **kwargs)
161        self.phaseshash = None
162
163    def copy(self):
164        """return an deep copy of the branchcache object"""
165        if util.safehasattr(self, '_entries'):
166            _entries = self._entries
167        else:
168            # hg <= 4.9 (624d6683c705+b137a6793c51)
169            _entries = self
170        args = (_entries, self.tipnode, self.tiprev, self.filteredhash,
171                self._closednodes)
172        if util.safehasattr(self, '_repo'):
173            # hg <= 5.7 (6266d19556ad)
174            args = (self._repo,) + args
175        new = self.__class__(*args)
176        new.phaseshash = self.phaseshash
177        return new
178
179    def validfor(self, repo):
180        """Is the cache content valid regarding a repo
181
182        - False when cached tipnode is unknown or if we detect a strip.
183        - True when cache is up to date or a subset of current repo."""
184        valid = super(_topiccache, self).validfor(repo)
185        if not valid:
186            return False
187        elif self.phaseshash is None:
188            # phasehash at None means this is a branchmap
189            # coming from a public only set
190            return True
191        else:
192            try:
193                valid = self.phaseshash == _phaseshash(repo, self.tiprev)
194                return valid
195            except IndexError:
196                return False
197
198    def write(self, repo):
199        # we expect (hope) mutable set to be small enough to be that computing
200        # it all the time will be fast enough
201        if not mighttopic(repo):
202            super(_topiccache, self).write(repo)
203
204    def update(self, repo, revgen):
205        """Given a branchhead cache, self, that may have extra nodes or be
206        missing heads, and a generator of nodes that are strictly a superset of
207        heads missing, this function updates self to be correct.
208        """
209        super(_topiccache, self).update(repo, revgen)
210        if mighttopic(repo):
211            self.phaseshash = _phaseshash(repo, self.tiprev)
212
213def wrapread(orig, repo):
214    # Avoiding to write cache for filter where topic applies is a good step,
215    # but we need to also avoid reading it. Existing branchmap cache might
216    # exists before the turned the feature on.
217    if mighttopic(repo):
218        return None
219    return orig(repo)
220
221# advertise topic capabilities
222
223def wireprotocaps(orig, repo, proto):
224    caps = orig(repo, proto)
225    if hasminitopic(repo):
226        caps.append(b'topics')
227    return caps
228
229# wrap the necessary bit
230
231def wrapclass(container, oldname, new):
232    old = getattr(container, oldname)
233    if not issubclass(old, new):
234        targetclass = new
235        # check if someone else already wrapped the class and handle that
236        if not issubclass(new, old):
237            class targetclass(new, old):
238                pass
239        setattr(container, oldname, targetclass)
240    current = getattr(container, oldname)
241    assert issubclass(current, new), (current, new, targetclass)
242
243def uisetup(ui):
244    wrapclass(branchmap, 'branchcache', _topiccache)
245    try:
246        # hg <= 4.9 (3461814417f3)
247        extensions.wrapfunction(branchmap, 'read', wrapread)
248    except AttributeError:
249        # Mercurial 5.0; branchcache.fromfile now takes care of this
250        # which is alredy defined on _topiccache
251        pass
252    extensions.wrapfunction(wireprotov1server, '_capabilities', wireprotocaps)
253    extensions.wrapfunction(context.changectx, 'branch', topicbranch)
254