|
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 def wrapread(orig, repo): |
|
193 # Avoiding to write cache for filter where topic applies is a good step, |
|
194 # but we need to also avoid reading it. Existing branchmap cache might |
|
195 # exists before the turned the feature on. |
|
196 if mighttopic(repo): |
|
197 return None |
|
198 return orig(repo) |
|
199 |
|
200 # advertise topic capabilities |
|
201 |
|
202 def wireprotocaps(orig, repo, proto): |
|
203 caps = orig(repo, proto) |
|
204 if hasminitopic(repo): |
|
205 caps.append('topics') |
|
206 return caps |
|
207 |
|
208 # wrap the necessary bit |
|
209 |
|
210 def wrapclass(container, oldname, new): |
|
211 old = getattr(container, oldname) |
|
212 if not issubclass(old, new): |
|
213 targetclass = new |
|
214 # check if someone else already wrapped the class and handle that |
|
215 if not issubclass(new, old): |
|
216 class targetclass(new, old): |
|
217 pass |
|
218 setattr(container, oldname, targetclass) |
|
219 current = getattr(container, oldname) |
|
220 assert issubclass(current, new), (current, new, targetclass) |
|
221 |
|
222 def uisetup(ui): |
|
223 wrapclass(context, 'changectx', topicchangectx) |
|
224 wrapclass(branchmap, 'branchcache', _topiccache) |
|
225 extensions.wrapfunction(branchmap, 'read', wrapread) |
|
226 extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps) |