##// 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__):
230 cl.__class__ = type(
231 'filteredchangelog', (filteredchangelogmixin, cl.__class__), {}
232 )
233
234 return cl
235
236
237 class filteredchangelogmixin(object):
231 238 def tiprev(self):
232 239 """filtered version of revlog.tiprev"""
233 240 for i in pycompat.xrange(len(self) - 1, -2, -1):
234 241 if i not in self.filteredrevs:
235 242 return i
236 243
237 244 def __contains__(self, rev):
238 245 """filtered version of revlog.__contains__"""
239 246 return 0 <= rev < len(self) and rev not in self.filteredrevs
240 247
241 248 def __iter__(self):
242 249 """filtered version of revlog.__iter__"""
243 250
244 251 def filterediter():
245 252 for i in pycompat.xrange(len(self)):
246 253 if i not in self.filteredrevs:
247 254 yield i
248 255
249 256 return filterediter()
250 257
251 258 def revs(self, start=0, stop=None):
252 259 """filtered version of revlog.revs"""
253 for i in super(filteredchangelog, self).revs(start, stop):
260 for i in super(filteredchangelogmixin, self).revs(start, stop):
254 261 if i not in self.filteredrevs:
255 262 yield i
256 263
257 264 def _checknofilteredinrevs(self, revs):
258 265 """raise the appropriate error if 'revs' contains a filtered revision
259 266
260 267 This returns a version of 'revs' to be used thereafter by the caller.
261 268 In particular, if revs is an iterator, it is converted into a set.
262 269 """
263 270 safehasattr = util.safehasattr
264 271 if safehasattr(revs, '__next__'):
265 272 # Note that inspect.isgenerator() is not true for iterators,
266 273 revs = set(revs)
267 274
268 275 filteredrevs = self.filteredrevs
269 276 if safehasattr(revs, 'first'): # smartset
270 277 offenders = revs & filteredrevs
271 278 else:
272 279 offenders = filteredrevs.intersection(revs)
273 280
274 281 for rev in offenders:
275 282 raise error.FilteredIndexError(rev)
276 283 return revs
277 284
278 285 def headrevs(self, revs=None):
279 286 if revs is None:
280 287 try:
281 288 return self.index.headrevsfiltered(self.filteredrevs)
282 289 # AttributeError covers non-c-extension environments and
283 290 # old c extensions without filter handling.
284 291 except AttributeError:
285 292 return self._headrevs()
286 293
287 294 revs = self._checknofilteredinrevs(revs)
288 return super(filteredchangelog, self).headrevs(revs)
295 return super(filteredchangelogmixin, self).headrevs(revs)
289 296
290 297 def strip(self, *args, **kwargs):
291 298 # XXX make something better than assert
292 299 # We can't expect proper strip behavior if we are filtered.
293 300 assert not self.filteredrevs
294 super(filteredchangelog, self).strip(*args, **kwargs)
301 super(filteredchangelogmixin, self).strip(*args, **kwargs)
295 302
296 303 def rev(self, node):
297 304 """filtered version of revlog.rev"""
298 r = super(filteredchangelog, self).rev(node)
305 r = super(filteredchangelogmixin, self).rev(node)
299 306 if r in self.filteredrevs:
300 307 raise error.FilteredLookupError(
301 308 hex(node), self.indexfile, _(b'filtered node')
302 309 )
303 310 return r
304 311
305 312 def node(self, rev):
306 313 """filtered version of revlog.node"""
307 314 if rev in self.filteredrevs:
308 315 raise error.FilteredIndexError(rev)
309 return super(filteredchangelog, self).node(rev)
316 return super(filteredchangelogmixin, self).node(rev)
310 317
311 318 def linkrev(self, rev):
312 319 """filtered version of revlog.linkrev"""
313 320 if rev in self.filteredrevs:
314 321 raise error.FilteredIndexError(rev)
315 return super(filteredchangelog, self).linkrev(rev)
322 return super(filteredchangelogmixin, self).linkrev(rev)
316 323
317 324 def parentrevs(self, rev):
318 325 """filtered version of revlog.parentrevs"""
319 326 if rev in self.filteredrevs:
320 327 raise error.FilteredIndexError(rev)
321 return super(filteredchangelog, self).parentrevs(rev)
328 return super(filteredchangelogmixin, self).parentrevs(rev)
322 329
323 330 def flags(self, rev):
324 331 """filtered version of revlog.flags"""
325 332 if rev in self.filteredrevs:
326 333 raise error.FilteredIndexError(rev)
327 return super(filteredchangelog, self).flags(rev)
328
329 cl.__class__ = filteredchangelog
330
331 return cl
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