##// END OF EJS Templates
repoview: define filteredchangelog as a top-level (non-local) class...
Martin von Zweigbergk -
r43797:bad4a26b default
parent child Browse files
Show More
@@ -1,450 +1,453 b''
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 .i18n import _
15 15 from .node import (
16 16 hex,
17 17 nullrev,
18 18 )
19 19 from .pycompat import (
20 20 delattr,
21 21 getattr,
22 22 setattr,
23 23 )
24 24 from . import (
25 25 error,
26 26 obsolete,
27 27 phases,
28 28 pycompat,
29 29 tags as tagsmod,
30 30 util,
31 31 )
32 32 from .utils import repoviewutil
33 33
34 34
35 35 def hideablerevs(repo):
36 36 """Revision candidates to be hidden
37 37
38 38 This is a standalone function to allow extensions to wrap it.
39 39
40 40 Because we use the set of immutable changesets as a fallback subset in
41 41 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
42 42 "public" changesets as "hideable". Doing so would break multiple code
43 43 assertions and lead to crashes."""
44 44 obsoletes = obsolete.getrevs(repo, b'obsolete')
45 45 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
46 46 internals = frozenset(internals)
47 47 return obsoletes | internals
48 48
49 49
50 50 def pinnedrevs(repo):
51 51 """revisions blocking hidden changesets from being filtered
52 52 """
53 53
54 54 cl = repo.changelog
55 55 pinned = set()
56 56 pinned.update([par.rev() for par in repo[None].parents()])
57 57 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
58 58
59 59 tags = {}
60 60 tagsmod.readlocaltags(repo.ui, repo, tags, {})
61 61 if tags:
62 62 rev, nodemap = cl.rev, cl.nodemap
63 63 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
64 64 return pinned
65 65
66 66
67 67 def _revealancestors(pfunc, hidden, revs):
68 68 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
69 69 from 'hidden'
70 70
71 71 - pfunc(r): a funtion returning parent of 'r',
72 72 - hidden: the (preliminary) hidden revisions, to be updated
73 73 - revs: iterable of revnum,
74 74
75 75 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
76 76 *not* revealed)
77 77 """
78 78 stack = list(revs)
79 79 while stack:
80 80 for p in pfunc(stack.pop()):
81 81 if p != nullrev and p in hidden:
82 82 hidden.remove(p)
83 83 stack.append(p)
84 84
85 85
86 86 def computehidden(repo, visibilityexceptions=None):
87 87 """compute the set of hidden revision to filter
88 88
89 89 During most operation hidden should be filtered."""
90 90 assert not repo.changelog.filteredrevs
91 91
92 92 hidden = hideablerevs(repo)
93 93 if hidden:
94 94 hidden = set(hidden - pinnedrevs(repo))
95 95 if visibilityexceptions:
96 96 hidden -= visibilityexceptions
97 97 pfunc = repo.changelog.parentrevs
98 98 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
99 99
100 100 visible = mutable - hidden
101 101 _revealancestors(pfunc, hidden, visible)
102 102 return frozenset(hidden)
103 103
104 104
105 105 def computesecret(repo, visibilityexceptions=None):
106 106 """compute the set of revision that can never be exposed through hgweb
107 107
108 108 Changeset in the secret phase (or above) should stay unaccessible."""
109 109 assert not repo.changelog.filteredrevs
110 110 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
111 111 return frozenset(secrets)
112 112
113 113
114 114 def computeunserved(repo, visibilityexceptions=None):
115 115 """compute the set of revision that should be filtered when used a server
116 116
117 117 Secret and hidden changeset should not pretend to be here."""
118 118 assert not repo.changelog.filteredrevs
119 119 # fast path in simple case to avoid impact of non optimised code
120 120 hiddens = filterrevs(repo, b'visible')
121 121 secrets = filterrevs(repo, b'served.hidden')
122 122 if secrets:
123 123 return frozenset(hiddens | secrets)
124 124 else:
125 125 return hiddens
126 126
127 127
128 128 def computemutable(repo, visibilityexceptions=None):
129 129 assert not repo.changelog.filteredrevs
130 130 # fast check to avoid revset call on huge repo
131 131 if any(repo._phasecache.phaseroots[1:]):
132 132 getphase = repo._phasecache.phase
133 133 maymutable = filterrevs(repo, b'base')
134 134 return frozenset(r for r in maymutable if getphase(repo, r))
135 135 return frozenset()
136 136
137 137
138 138 def computeimpactable(repo, visibilityexceptions=None):
139 139 """Everything impactable by mutable revision
140 140
141 141 The immutable filter still have some chance to get invalidated. This will
142 142 happen when:
143 143
144 144 - you garbage collect hidden changeset,
145 145 - public phase is moved backward,
146 146 - something is changed in the filtering (this could be fixed)
147 147
148 148 This filter out any mutable changeset and any public changeset that may be
149 149 impacted by something happening to a mutable revision.
150 150
151 151 This is achieved by filtered everything with a revision number egal or
152 152 higher than the first mutable changeset is filtered."""
153 153 assert not repo.changelog.filteredrevs
154 154 cl = repo.changelog
155 155 firstmutable = len(cl)
156 156 for roots in repo._phasecache.phaseroots[1:]:
157 157 if roots:
158 158 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
159 159 # protect from nullrev root
160 160 firstmutable = max(0, firstmutable)
161 161 return frozenset(pycompat.xrange(firstmutable, len(cl)))
162 162
163 163
164 164 # function to compute filtered set
165 165 #
166 166 # When adding a new filter you MUST update the table at:
167 167 # mercurial.utils.repoviewutil.subsettable
168 168 # Otherwise your filter will have to recompute all its branches cache
169 169 # from scratch (very slow).
170 170 filtertable = {
171 171 b'visible': computehidden,
172 172 b'visible-hidden': computehidden,
173 173 b'served.hidden': computesecret,
174 174 b'served': computeunserved,
175 175 b'immutable': computemutable,
176 176 b'base': computeimpactable,
177 177 }
178 178
179 179 _basefiltername = list(filtertable)
180 180
181 181
182 182 def extrafilter(ui):
183 183 """initialize extra filter and return its id
184 184
185 185 If extra filtering is configured, we make sure the associated filtered view
186 186 are declared and return the associated id.
187 187 """
188 188 frevs = ui.config(b'experimental', b'extra-filter-revs')
189 189 if frevs is None:
190 190 return None
191 191
192 192 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
193 193
194 194 combine = lambda fname: fname + b'%' + fid
195 195
196 196 subsettable = repoviewutil.subsettable
197 197
198 198 if combine(b'base') not in filtertable:
199 199 for name in _basefiltername:
200 200
201 201 def extrafilteredrevs(repo, *args, **kwargs):
202 202 baserevs = filtertable[name](repo, *args, **kwargs)
203 203 extrarevs = frozenset(repo.revs(frevs))
204 204 return baserevs | extrarevs
205 205
206 206 filtertable[combine(name)] = extrafilteredrevs
207 207 if name in subsettable:
208 208 subsettable[combine(name)] = combine(subsettable[name])
209 209 return fid
210 210
211 211
212 212 def filterrevs(repo, filtername, visibilityexceptions=None):
213 213 """returns set of filtered revision for this filter name
214 214
215 215 visibilityexceptions is a set of revs which must are exceptions for
216 216 hidden-state and must be visible. They are dynamic and hence we should not
217 217 cache it's result"""
218 218 if filtername not in repo.filteredrevcache:
219 219 func = filtertable[filtername]
220 220 if visibilityexceptions:
221 221 return func(repo.unfiltered, visibilityexceptions)
222 222 repo.filteredrevcache[filtername] = func(repo.unfiltered())
223 223 return repo.filteredrevcache[filtername]
224 224
225 225
226 226 def wrapchangelog(unfichangelog, filteredrevs):
227 227 cl = copy.copy(unfichangelog)
228 228 cl.filteredrevs = filteredrevs
229 229
230 class filteredchangelog(cl.__class__):
231 def tiprev(self):
232 """filtered version of revlog.tiprev"""
233 for i in pycompat.xrange(len(self) - 1, -2, -1):
234 if i not in self.filteredrevs:
235 return i
230 cl.__class__ = type(
231 'filteredchangelog', (filteredchangelogmixin, cl.__class__), {}
232 )
236 233
237 def __contains__(self, rev):
238 """filtered version of revlog.__contains__"""
239 return 0 <= rev < len(self) and rev not in self.filteredrevs
234 return cl
235
240 236
241 def __iter__(self):
242 """filtered version of revlog.__iter__"""
237 class filteredchangelogmixin(object):
238 def tiprev(self):
239 """filtered version of revlog.tiprev"""
240 for i in pycompat.xrange(len(self) - 1, -2, -1):
241 if i not in self.filteredrevs:
242 return i
243 243
244 def filterediter():
245 for i in pycompat.xrange(len(self)):
246 if i not in self.filteredrevs:
247 yield i
244 def __contains__(self, rev):
245 """filtered version of revlog.__contains__"""
246 return 0 <= rev < len(self) and rev not in self.filteredrevs
248 247
249 return filterediter()
248 def __iter__(self):
249 """filtered version of revlog.__iter__"""
250 250
251 def revs(self, start=0, stop=None):
252 """filtered version of revlog.revs"""
253 for i in super(filteredchangelog, self).revs(start, stop):
251 def filterediter():
252 for i in pycompat.xrange(len(self)):
254 253 if i not in self.filteredrevs:
255 254 yield i
256 255
257 def _checknofilteredinrevs(self, revs):
258 """raise the appropriate error if 'revs' contains a filtered revision
256 return filterediter()
257
258 def revs(self, start=0, stop=None):
259 """filtered version of revlog.revs"""
260 for i in super(filteredchangelogmixin, self).revs(start, stop):
261 if i not in self.filteredrevs:
262 yield i
259 263
260 This returns a version of 'revs' to be used thereafter by the caller.
261 In particular, if revs is an iterator, it is converted into a set.
262 """
263 safehasattr = util.safehasattr
264 if safehasattr(revs, '__next__'):
265 # Note that inspect.isgenerator() is not true for iterators,
266 revs = set(revs)
264 def _checknofilteredinrevs(self, revs):
265 """raise the appropriate error if 'revs' contains a filtered revision
266
267 This returns a version of 'revs' to be used thereafter by the caller.
268 In particular, if revs is an iterator, it is converted into a set.
269 """
270 safehasattr = util.safehasattr
271 if safehasattr(revs, '__next__'):
272 # Note that inspect.isgenerator() is not true for iterators,
273 revs = set(revs)
267 274
268 filteredrevs = self.filteredrevs
269 if safehasattr(revs, 'first'): # smartset
270 offenders = revs & filteredrevs
271 else:
272 offenders = filteredrevs.intersection(revs)
273
274 for rev in offenders:
275 raise error.FilteredIndexError(rev)
276 return revs
275 filteredrevs = self.filteredrevs
276 if safehasattr(revs, 'first'): # smartset
277 offenders = revs & filteredrevs
278 else:
279 offenders = filteredrevs.intersection(revs)
277 280
278 def headrevs(self, revs=None):
279 if revs is None:
280 try:
281 return self.index.headrevsfiltered(self.filteredrevs)
282 # AttributeError covers non-c-extension environments and
283 # old c extensions without filter handling.
284 except AttributeError:
285 return self._headrevs()
281 for rev in offenders:
282 raise error.FilteredIndexError(rev)
283 return revs
286 284
287 revs = self._checknofilteredinrevs(revs)
288 return super(filteredchangelog, self).headrevs(revs)
285 def headrevs(self, revs=None):
286 if revs is None:
287 try:
288 return self.index.headrevsfiltered(self.filteredrevs)
289 # AttributeError covers non-c-extension environments and
290 # old c extensions without filter handling.
291 except AttributeError:
292 return self._headrevs()
289 293
290 def strip(self, *args, **kwargs):
291 # XXX make something better than assert
292 # We can't expect proper strip behavior if we are filtered.
293 assert not self.filteredrevs
294 super(filteredchangelog, self).strip(*args, **kwargs)
294 revs = self._checknofilteredinrevs(revs)
295 return super(filteredchangelogmixin, self).headrevs(revs)
296
297 def strip(self, *args, **kwargs):
298 # XXX make something better than assert
299 # We can't expect proper strip behavior if we are filtered.
300 assert not self.filteredrevs
301 super(filteredchangelogmixin, self).strip(*args, **kwargs)
295 302
296 def rev(self, node):
297 """filtered version of revlog.rev"""
298 r = super(filteredchangelog, self).rev(node)
299 if r in self.filteredrevs:
300 raise error.FilteredLookupError(
301 hex(node), self.indexfile, _(b'filtered node')
302 )
303 return r
304
305 def node(self, rev):
306 """filtered version of revlog.node"""
307 if rev in self.filteredrevs:
308 raise error.FilteredIndexError(rev)
309 return super(filteredchangelog, self).node(rev)
303 def rev(self, node):
304 """filtered version of revlog.rev"""
305 r = super(filteredchangelogmixin, self).rev(node)
306 if r in self.filteredrevs:
307 raise error.FilteredLookupError(
308 hex(node), self.indexfile, _(b'filtered node')
309 )
310 return r
310 311
311 def linkrev(self, rev):
312 """filtered version of revlog.linkrev"""
313 if rev in self.filteredrevs:
314 raise error.FilteredIndexError(rev)
315 return super(filteredchangelog, self).linkrev(rev)
312 def node(self, rev):
313 """filtered version of revlog.node"""
314 if rev in self.filteredrevs:
315 raise error.FilteredIndexError(rev)
316 return super(filteredchangelogmixin, self).node(rev)
317
318 def linkrev(self, rev):
319 """filtered version of revlog.linkrev"""
320 if rev in self.filteredrevs:
321 raise error.FilteredIndexError(rev)
322 return super(filteredchangelogmixin, self).linkrev(rev)
316 323
317 def parentrevs(self, rev):
318 """filtered version of revlog.parentrevs"""
319 if rev in self.filteredrevs:
320 raise error.FilteredIndexError(rev)
321 return super(filteredchangelog, self).parentrevs(rev)
324 def parentrevs(self, rev):
325 """filtered version of revlog.parentrevs"""
326 if rev in self.filteredrevs:
327 raise error.FilteredIndexError(rev)
328 return super(filteredchangelogmixin, self).parentrevs(rev)
322 329
323 def flags(self, rev):
324 """filtered version of revlog.flags"""
325 if rev in self.filteredrevs:
326 raise error.FilteredIndexError(rev)
327 return super(filteredchangelog, self).flags(rev)
328
329 cl.__class__ = filteredchangelog
330
331 return cl
330 def flags(self, rev):
331 """filtered version of revlog.flags"""
332 if rev in self.filteredrevs:
333 raise error.FilteredIndexError(rev)
334 return super(filteredchangelogmixin, self).flags(rev)
332 335
333 336
334 337 class repoview(object):
335 338 """Provide a read/write view of a repo through a filtered changelog
336 339
337 340 This object is used to access a filtered version of a repository without
338 341 altering the original repository object itself. We can not alter the
339 342 original object for two main reasons:
340 343 - It prevents the use of a repo with multiple filters at the same time. In
341 344 particular when multiple threads are involved.
342 345 - It makes scope of the filtering harder to control.
343 346
344 347 This object behaves very closely to the original repository. All attribute
345 348 operations are done on the original repository:
346 349 - An access to `repoview.someattr` actually returns `repo.someattr`,
347 350 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
348 351 - A deletion of `repoview.someattr` actually drops `someattr`
349 352 from `repo.__dict__`.
350 353
351 354 The only exception is the `changelog` property. It is overridden to return
352 355 a (surface) copy of `repo.changelog` with some revisions filtered. The
353 356 `filtername` attribute of the view control the revisions that need to be
354 357 filtered. (the fact the changelog is copied is an implementation detail).
355 358
356 359 Unlike attributes, this object intercepts all method calls. This means that
357 360 all methods are run on the `repoview` object with the filtered `changelog`
358 361 property. For this purpose the simple `repoview` class must be mixed with
359 362 the actual class of the repository. This ensures that the resulting
360 363 `repoview` object have the very same methods than the repo object. This
361 364 leads to the property below.
362 365
363 366 repoview.method() --> repo.__class__.method(repoview)
364 367
365 368 The inheritance has to be done dynamically because `repo` can be of any
366 369 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
367 370 """
368 371
369 372 def __init__(self, repo, filtername, visibilityexceptions=None):
370 373 object.__setattr__(self, r'_unfilteredrepo', repo)
371 374 object.__setattr__(self, r'filtername', filtername)
372 375 object.__setattr__(self, r'_clcachekey', None)
373 376 object.__setattr__(self, r'_clcache', None)
374 377 # revs which are exceptions and must not be hidden
375 378 object.__setattr__(self, r'_visibilityexceptions', visibilityexceptions)
376 379
377 380 # not a propertycache on purpose we shall implement a proper cache later
378 381 @property
379 382 def changelog(self):
380 383 """return a filtered version of the changeset
381 384
382 385 this changelog must not be used for writing"""
383 386 # some cache may be implemented later
384 387 unfi = self._unfilteredrepo
385 388 unfichangelog = unfi.changelog
386 389 # bypass call to changelog.method
387 390 unfiindex = unfichangelog.index
388 391 unfilen = len(unfiindex)
389 392 unfinode = unfiindex[unfilen - 1][7]
390 393 with util.timedcm('repo filter for %s', self.filtername):
391 394 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
392 395 cl = self._clcache
393 396 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
394 397 # if cl.index is not unfiindex, unfi.changelog would be
395 398 # recreated, and our clcache refers to garbage object
396 399 if cl is not None and (
397 400 cl.index is not unfiindex or newkey != self._clcachekey
398 401 ):
399 402 cl = None
400 403 # could have been made None by the previous if
401 404 if cl is None:
402 405 # Only filter if there's something to filter
403 406 cl = wrapchangelog(unfichangelog, revs) if revs else unfichangelog
404 407 object.__setattr__(self, r'_clcache', cl)
405 408 object.__setattr__(self, r'_clcachekey', newkey)
406 409 return cl
407 410
408 411 def unfiltered(self):
409 412 """Return an unfiltered version of a repo"""
410 413 return self._unfilteredrepo
411 414
412 415 def filtered(self, name, visibilityexceptions=None):
413 416 """Return a filtered version of a repository"""
414 417 if name == self.filtername and not visibilityexceptions:
415 418 return self
416 419 return self.unfiltered().filtered(name, visibilityexceptions)
417 420
418 421 def __repr__(self):
419 422 return r'<%s:%s %r>' % (
420 423 self.__class__.__name__,
421 424 pycompat.sysstr(self.filtername),
422 425 self.unfiltered(),
423 426 )
424 427
425 428 # everything access are forwarded to the proxied repo
426 429 def __getattr__(self, attr):
427 430 return getattr(self._unfilteredrepo, attr)
428 431
429 432 def __setattr__(self, attr, value):
430 433 return setattr(self._unfilteredrepo, attr, value)
431 434
432 435 def __delattr__(self, attr):
433 436 return delattr(self._unfilteredrepo, attr)
434 437
435 438
436 439 # Python <3.4 easily leaks types via __mro__. See
437 440 # https://bugs.python.org/issue17950. We cache dynamically created types
438 441 # so they won't be leaked on every invocation of repo.filtered().
439 442 _filteredrepotypes = weakref.WeakKeyDictionary()
440 443
441 444
442 445 def newtype(base):
443 446 """Create a new type with the repoview mixin and the given base class"""
444 447 if base not in _filteredrepotypes:
445 448
446 449 class filteredrepo(repoview, base):
447 450 pass
448 451
449 452 _filteredrepotypes[base] = filteredrepo
450 453 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now