comparison hgext3rd/serverminitopic.py @ 3206:3ccde4699cf0

topic: introduce a minimal extensions to enable topic on the server This small extensions simply expose topic in the branch. This should help getting minimal support for topic from various hosting solution (eg: bitbucket maybe ?) See extensions help for details.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Wed, 15 Nov 2017 08:00:17 +0100
parents
children 35c79686a635
comparison
equal deleted inserted replaced
3205:0b85da2e8e2a 3206:3ccde4699cf0
1 """enable a minimal verison of topic for server
2
3 Non publishing repository will see topic as "branch:topic" in the branch field.
4
5 In addition to adding the extensions, the feature must be manually enabled in the config:
6
7 [experimental]
8 server-mini-topic = yes
9 """
10 import hashlib
11 import contextlib
12
13 from mercurial import (
14 branchmap,
15 context,
16 encoding,
17 extensions,
18 node,
19 registrar,
20 util,
21 wireproto,
22 )
23
24 if util.safehasattr(registrar, 'configitem'):
25
26 configtable = {}
27 configitem = registrar.configitem(configtable)
28 configitem('experimental', 'server-mini-topic',
29 default=False,
30 )
31
32 def hasminitopic(repo):
33 """true if minitopic is enabled on the repository
34
35 (The value is cached on the repository)
36 """
37 enabled = getattr(repo, '_hasminitopic', None)
38 if enabled is None:
39 enabled = (repo.ui.configbool('experimental', 'server-mini-topic')
40 and not repo.publishing())
41 repo._hasminitopic = enabled
42 return enabled
43
44 ### make topic visible though "ctx.branch()"
45
46 class topicchangectx(context.changectx):
47 """a sunclass of changectx that add topic to the branch name"""
48
49 def branch(self):
50 branch = super(topicchangectx, self).branch()
51 if hasminitopic(self._repo) and self.phase():
52 topic = self._changeset.extra.get('topic')
53 if topic is not None:
54 topic = encoding.tolocal(topic)
55 branch = '%s:%s' % (branch, topic)
56 return branch
57
58 ### avoid caching topic data in rev-branch-cache
59
60 class revbranchcacheoverlay(object):
61 """revbranch mixin that don't use the cache for non public changeset"""
62
63 def _init__(self, *args, **kwargs):
64 super(revbranchcacheoverlay, self).__init__(*args, **kwargs)
65 if 'branchinfo' in vars(self):
66 del self.branchinfo
67
68 def branchinfo(self, rev):
69 """return branch name and close flag for rev, using and updating
70 persistent cache."""
71 phase = self._repo._phasecache.phase(self, rev)
72 if phase:
73 ctx = self._repo[rev]
74 return ctx.branch(), ctx.closesbranch()
75 return super(revbranchcacheoverlay, self).branchinfo(rev)
76
77 def reposetup(ui, repo):
78 """install a repo class with a special revbranchcache"""
79
80 if hasminitopic(repo):
81 repo = repo.unfiltered()
82
83 class minitopicrepo(repo.__class__):
84 """repository subclass that install the modified cache"""
85
86 def revbranchcache(self):
87 if self._revbranchcache is None:
88 cache = super(minitopicrepo, self).revbranchcache()
89
90 class topicawarerbc(revbranchcacheoverlay, cache.__class__):
91 pass
92 cache.__class__ = topicawarerbc
93 if 'branchinfo' in vars(cache):
94 del cache.branchinfo
95 self._revbranchcache = cache
96 return self._revbranchcache
97
98 repo.__class__ = minitopicrepo
99
100 ### topic aware branch head cache
101
102 def _phaseshash(repo, maxrev):
103 """uniq ID for a phase matching a set of rev"""
104 revs = set()
105 cl = repo.changelog
106 fr = cl.filteredrevs
107 nm = cl.nodemap
108 for roots in repo._phasecache.phaseroots[1:]:
109 for n in roots:
110 r = nm.get(n)
111 if r not in fr and r < maxrev:
112 revs.add(r)
113 key = node.nullid
114 revs = sorted(revs)
115 if revs:
116 s = hashlib.sha1()
117 for rev in revs:
118 s.update('%s;' % rev)
119 key = s.digest()
120 return key
121
122 # needed to prevent reference used for 'super()' call using in branchmap.py to
123 # no go into cycle. (yes, URG)
124 _oldbranchmap = branchmap.branchcache
125
126 @contextlib.contextmanager
127 def oldbranchmap():
128 previous = branchmap.branchcache
129 try:
130 branchmap.branchcache = _oldbranchmap
131 yield
132 finally:
133 branchmap.branchcache = previous
134
135 _publiconly = set([
136 'base',
137 'immutable',
138 ])
139
140 def mighttopic(repo):
141 return hasminitopic(repo) and repo.filtername not in _publiconly
142
143 class _topiccache(branchmap.branchcache): # combine me with branchmap.branchcache
144
145 def __init__(self, *args, **kwargs):
146 # super() call may fail otherwise
147 with oldbranchmap():
148 super(_topiccache, self).__init__(*args, **kwargs)
149 self.phaseshash = None
150
151 def copy(self):
152 """return an deep copy of the branchcache object"""
153 new = self.__class__(self, self.tipnode, self.tiprev, self.filteredhash,
154 self._closednodes)
155 new.phaseshash = self.phaseshash
156 return new
157
158 def validfor(self, repo):
159 """Is the cache content valid regarding a repo
160
161 - False when cached tipnode is unknown or if we detect a strip.
162 - True when cache is up to date or a subset of current repo."""
163 valid = super(_topiccache, self).validfor(repo)
164 if not valid:
165 return False
166 elif not mighttopic(repo) and self.phaseshash is None:
167 # phasehash at None means this is a branchmap
168 # coming from a public only set
169 return True
170 else:
171 try:
172 valid = self.phaseshash == _phaseshash(repo, self.tiprev)
173 return valid
174 except IndexError:
175 return False
176
177 def write(self, repo):
178 # we expect (hope) mutable set to be small enough to be that computing
179 # it all the time will be fast enough
180 if not mighttopic(repo):
181 super(_topiccache, self).write(repo)
182
183 def update(self, repo, revgen):
184 """Given a branchhead cache, self, that may have extra nodes or be
185 missing heads, and a generator of nodes that are strictly a superset of
186 heads missing, this function updates self to be correct.
187 """
188 super(_topiccache, self).update(repo, revgen)
189 if mighttopic(repo):
190 self.phaseshash = _phaseshash(repo, self.tiprev)
191
192 # advertise topic capabilities
193
194 def wireprotocaps(orig, repo, proto):
195 caps = orig(repo, proto)
196 if hasminitopic(repo):
197 caps.append('topics')
198 return caps
199
200 # wrap the necessary bit
201
202 def wrapclass(container, oldname, new):
203 old = getattr(container, oldname)
204 if not issubclass(old, new):
205 targetclass = new
206 # check if someone else already wrapped the class and handle that
207 if not issubclass(new, old):
208 class targetclass(new, old):
209 pass
210 setattr(container, oldname, targetclass)
211 current = getattr(container, oldname)
212 assert issubclass(current, new), (current, new, targetclass)
213
214 def uisetup(ui):
215 wrapclass(context, 'changectx', topicchangectx)
216 wrapclass(branchmap, 'branchcache', _topiccache)
217 extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps)