Mercurial > evolve
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) |