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