##// END OF EJS Templates
repoview: wrap changelog class when filtering...
Martin von Zweigbergk -
r43747:625e7d1f default
parent child Browse files
Show More
@@ -1,342 +1,348
1 1 # repoview.py - Filtered view of a localrepo object
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import weakref
13 13
14 14 from .node import nullrev
15 15 from .pycompat import (
16 16 delattr,
17 17 getattr,
18 18 setattr,
19 19 )
20 20 from . import (
21 21 obsolete,
22 22 phases,
23 23 pycompat,
24 24 tags as tagsmod,
25 25 util,
26 26 )
27 27 from .utils import repoviewutil
28 28
29 29
30 30 def hideablerevs(repo):
31 31 """Revision candidates to be hidden
32 32
33 33 This is a standalone function to allow extensions to wrap it.
34 34
35 35 Because we use the set of immutable changesets as a fallback subset in
36 36 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
37 37 "public" changesets as "hideable". Doing so would break multiple code
38 38 assertions and lead to crashes."""
39 39 obsoletes = obsolete.getrevs(repo, b'obsolete')
40 40 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
41 41 internals = frozenset(internals)
42 42 return obsoletes | internals
43 43
44 44
45 45 def pinnedrevs(repo):
46 46 """revisions blocking hidden changesets from being filtered
47 47 """
48 48
49 49 cl = repo.changelog
50 50 pinned = set()
51 51 pinned.update([par.rev() for par in repo[None].parents()])
52 52 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
53 53
54 54 tags = {}
55 55 tagsmod.readlocaltags(repo.ui, repo, tags, {})
56 56 if tags:
57 57 rev, nodemap = cl.rev, cl.nodemap
58 58 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
59 59 return pinned
60 60
61 61
62 62 def _revealancestors(pfunc, hidden, revs):
63 63 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
64 64 from 'hidden'
65 65
66 66 - pfunc(r): a funtion returning parent of 'r',
67 67 - hidden: the (preliminary) hidden revisions, to be updated
68 68 - revs: iterable of revnum,
69 69
70 70 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
71 71 *not* revealed)
72 72 """
73 73 stack = list(revs)
74 74 while stack:
75 75 for p in pfunc(stack.pop()):
76 76 if p != nullrev and p in hidden:
77 77 hidden.remove(p)
78 78 stack.append(p)
79 79
80 80
81 81 def computehidden(repo, visibilityexceptions=None):
82 82 """compute the set of hidden revision to filter
83 83
84 84 During most operation hidden should be filtered."""
85 85 assert not repo.changelog.filteredrevs
86 86
87 87 hidden = hideablerevs(repo)
88 88 if hidden:
89 89 hidden = set(hidden - pinnedrevs(repo))
90 90 if visibilityexceptions:
91 91 hidden -= visibilityexceptions
92 92 pfunc = repo.changelog.parentrevs
93 93 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
94 94
95 95 visible = mutable - hidden
96 96 _revealancestors(pfunc, hidden, visible)
97 97 return frozenset(hidden)
98 98
99 99
100 100 def computesecret(repo, visibilityexceptions=None):
101 101 """compute the set of revision that can never be exposed through hgweb
102 102
103 103 Changeset in the secret phase (or above) should stay unaccessible."""
104 104 assert not repo.changelog.filteredrevs
105 105 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
106 106 return frozenset(secrets)
107 107
108 108
109 109 def computeunserved(repo, visibilityexceptions=None):
110 110 """compute the set of revision that should be filtered when used a server
111 111
112 112 Secret and hidden changeset should not pretend to be here."""
113 113 assert not repo.changelog.filteredrevs
114 114 # fast path in simple case to avoid impact of non optimised code
115 115 hiddens = filterrevs(repo, b'visible')
116 116 secrets = filterrevs(repo, b'served.hidden')
117 117 if secrets:
118 118 return frozenset(hiddens | secrets)
119 119 else:
120 120 return hiddens
121 121
122 122
123 123 def computemutable(repo, visibilityexceptions=None):
124 124 assert not repo.changelog.filteredrevs
125 125 # fast check to avoid revset call on huge repo
126 126 if any(repo._phasecache.phaseroots[1:]):
127 127 getphase = repo._phasecache.phase
128 128 maymutable = filterrevs(repo, b'base')
129 129 return frozenset(r for r in maymutable if getphase(repo, r))
130 130 return frozenset()
131 131
132 132
133 133 def computeimpactable(repo, visibilityexceptions=None):
134 134 """Everything impactable by mutable revision
135 135
136 136 The immutable filter still have some chance to get invalidated. This will
137 137 happen when:
138 138
139 139 - you garbage collect hidden changeset,
140 140 - public phase is moved backward,
141 141 - something is changed in the filtering (this could be fixed)
142 142
143 143 This filter out any mutable changeset and any public changeset that may be
144 144 impacted by something happening to a mutable revision.
145 145
146 146 This is achieved by filtered everything with a revision number egal or
147 147 higher than the first mutable changeset is filtered."""
148 148 assert not repo.changelog.filteredrevs
149 149 cl = repo.changelog
150 150 firstmutable = len(cl)
151 151 for roots in repo._phasecache.phaseroots[1:]:
152 152 if roots:
153 153 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
154 154 # protect from nullrev root
155 155 firstmutable = max(0, firstmutable)
156 156 return frozenset(pycompat.xrange(firstmutable, len(cl)))
157 157
158 158
159 159 # function to compute filtered set
160 160 #
161 161 # When adding a new filter you MUST update the table at:
162 162 # mercurial.utils.repoviewutil.subsettable
163 163 # Otherwise your filter will have to recompute all its branches cache
164 164 # from scratch (very slow).
165 165 filtertable = {
166 166 b'visible': computehidden,
167 167 b'visible-hidden': computehidden,
168 168 b'served.hidden': computesecret,
169 169 b'served': computeunserved,
170 170 b'immutable': computemutable,
171 171 b'base': computeimpactable,
172 172 }
173 173
174 174 _basefiltername = list(filtertable)
175 175
176 176
177 177 def extrafilter(ui):
178 178 """initialize extra filter and return its id
179 179
180 180 If extra filtering is configured, we make sure the associated filtered view
181 181 are declared and return the associated id.
182 182 """
183 183 frevs = ui.config(b'experimental', b'extra-filter-revs')
184 184 if frevs is None:
185 185 return None
186 186
187 187 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
188 188
189 189 combine = lambda fname: fname + b'%' + fid
190 190
191 191 subsettable = repoviewutil.subsettable
192 192
193 193 if combine(b'base') not in filtertable:
194 194 for name in _basefiltername:
195 195
196 196 def extrafilteredrevs(repo, *args, **kwargs):
197 197 baserevs = filtertable[name](repo, *args, **kwargs)
198 198 extrarevs = frozenset(repo.revs(frevs))
199 199 return baserevs | extrarevs
200 200
201 201 filtertable[combine(name)] = extrafilteredrevs
202 202 if name in subsettable:
203 203 subsettable[combine(name)] = combine(subsettable[name])
204 204 return fid
205 205
206 206
207 207 def filterrevs(repo, filtername, visibilityexceptions=None):
208 208 """returns set of filtered revision for this filter name
209 209
210 210 visibilityexceptions is a set of revs which must are exceptions for
211 211 hidden-state and must be visible. They are dynamic and hence we should not
212 212 cache it's result"""
213 213 if filtername not in repo.filteredrevcache:
214 214 func = filtertable[filtername]
215 215 if visibilityexceptions:
216 216 return func(repo.unfiltered, visibilityexceptions)
217 217 repo.filteredrevcache[filtername] = func(repo.unfiltered())
218 218 return repo.filteredrevcache[filtername]
219 219
220 220
221 221 def wrapchangelog(unfichangelog, filteredrevs):
222 222 cl = copy.copy(unfichangelog)
223 223 cl.filteredrevs = filteredrevs
224
225 class filteredchangelog(cl.__class__):
226 pass
227
228 cl.__class__ = filteredchangelog
229
224 230 return cl
225 231
226 232
227 233 class repoview(object):
228 234 """Provide a read/write view of a repo through a filtered changelog
229 235
230 236 This object is used to access a filtered version of a repository without
231 237 altering the original repository object itself. We can not alter the
232 238 original object for two main reasons:
233 239 - It prevents the use of a repo with multiple filters at the same time. In
234 240 particular when multiple threads are involved.
235 241 - It makes scope of the filtering harder to control.
236 242
237 243 This object behaves very closely to the original repository. All attribute
238 244 operations are done on the original repository:
239 245 - An access to `repoview.someattr` actually returns `repo.someattr`,
240 246 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
241 247 - A deletion of `repoview.someattr` actually drops `someattr`
242 248 from `repo.__dict__`.
243 249
244 250 The only exception is the `changelog` property. It is overridden to return
245 251 a (surface) copy of `repo.changelog` with some revisions filtered. The
246 252 `filtername` attribute of the view control the revisions that need to be
247 253 filtered. (the fact the changelog is copied is an implementation detail).
248 254
249 255 Unlike attributes, this object intercepts all method calls. This means that
250 256 all methods are run on the `repoview` object with the filtered `changelog`
251 257 property. For this purpose the simple `repoview` class must be mixed with
252 258 the actual class of the repository. This ensures that the resulting
253 259 `repoview` object have the very same methods than the repo object. This
254 260 leads to the property below.
255 261
256 262 repoview.method() --> repo.__class__.method(repoview)
257 263
258 264 The inheritance has to be done dynamically because `repo` can be of any
259 265 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
260 266 """
261 267
262 268 def __init__(self, repo, filtername, visibilityexceptions=None):
263 269 object.__setattr__(self, r'_unfilteredrepo', repo)
264 270 object.__setattr__(self, r'filtername', filtername)
265 271 object.__setattr__(self, r'_clcachekey', None)
266 272 object.__setattr__(self, r'_clcache', None)
267 273 # revs which are exceptions and must not be hidden
268 274 object.__setattr__(self, r'_visibilityexceptions', visibilityexceptions)
269 275
270 276 # not a propertycache on purpose we shall implement a proper cache later
271 277 @property
272 278 def changelog(self):
273 279 """return a filtered version of the changeset
274 280
275 281 this changelog must not be used for writing"""
276 282 # some cache may be implemented later
277 283 unfi = self._unfilteredrepo
278 284 unfichangelog = unfi.changelog
279 285 # bypass call to changelog.method
280 286 unfiindex = unfichangelog.index
281 287 unfilen = len(unfiindex)
282 288 unfinode = unfiindex[unfilen - 1][7]
283 289 with util.timedcm('repo filter for %s', self.filtername):
284 290 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
285 291 cl = self._clcache
286 292 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
287 293 # if cl.index is not unfiindex, unfi.changelog would be
288 294 # recreated, and our clcache refers to garbage object
289 295 if cl is not None and (
290 296 cl.index is not unfiindex or newkey != self._clcachekey
291 297 ):
292 298 cl = None
293 299 # could have been made None by the previous if
294 300 if cl is None:
295 301 cl = wrapchangelog(unfichangelog, revs)
296 302 object.__setattr__(self, r'_clcache', cl)
297 303 object.__setattr__(self, r'_clcachekey', newkey)
298 304 return cl
299 305
300 306 def unfiltered(self):
301 307 """Return an unfiltered version of a repo"""
302 308 return self._unfilteredrepo
303 309
304 310 def filtered(self, name, visibilityexceptions=None):
305 311 """Return a filtered version of a repository"""
306 312 if name == self.filtername and not visibilityexceptions:
307 313 return self
308 314 return self.unfiltered().filtered(name, visibilityexceptions)
309 315
310 316 def __repr__(self):
311 317 return r'<%s:%s %r>' % (
312 318 self.__class__.__name__,
313 319 pycompat.sysstr(self.filtername),
314 320 self.unfiltered(),
315 321 )
316 322
317 323 # everything access are forwarded to the proxied repo
318 324 def __getattr__(self, attr):
319 325 return getattr(self._unfilteredrepo, attr)
320 326
321 327 def __setattr__(self, attr, value):
322 328 return setattr(self._unfilteredrepo, attr, value)
323 329
324 330 def __delattr__(self, attr):
325 331 return delattr(self._unfilteredrepo, attr)
326 332
327 333
328 334 # Python <3.4 easily leaks types via __mro__. See
329 335 # https://bugs.python.org/issue17950. We cache dynamically created types
330 336 # so they won't be leaked on every invocation of repo.filtered().
331 337 _filteredrepotypes = weakref.WeakKeyDictionary()
332 338
333 339
334 340 def newtype(base):
335 341 """Create a new type with the repoview mixin and the given base class"""
336 342 if base not in _filteredrepotypes:
337 343
338 344 class filteredrepo(repoview, base):
339 345 pass
340 346
341 347 _filteredrepotypes[base] = filteredrepo
342 348 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now