##// END OF EJS Templates
repoview: use class literal for creating filteredchangelog...
Martin von Zweigbergk -
r43910:85628a59 default
parent child Browse files
Show More
@@ -1,453 +1,454 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 cl.__class__ = type(
231 'filteredchangelog', (filteredchangelogmixin, cl.__class__), {}
232 )
230 class filteredchangelog(filteredchangelogmixin, cl.__class__):
231 pass
232
233 cl.__class__ = filteredchangelog
233 234
234 235 return cl
235 236
236 237
237 238 class filteredchangelogmixin(object):
238 239 def tiprev(self):
239 240 """filtered version of revlog.tiprev"""
240 241 for i in pycompat.xrange(len(self) - 1, -2, -1):
241 242 if i not in self.filteredrevs:
242 243 return i
243 244
244 245 def __contains__(self, rev):
245 246 """filtered version of revlog.__contains__"""
246 247 return 0 <= rev < len(self) and rev not in self.filteredrevs
247 248
248 249 def __iter__(self):
249 250 """filtered version of revlog.__iter__"""
250 251
251 252 def filterediter():
252 253 for i in pycompat.xrange(len(self)):
253 254 if i not in self.filteredrevs:
254 255 yield i
255 256
256 257 return filterediter()
257 258
258 259 def revs(self, start=0, stop=None):
259 260 """filtered version of revlog.revs"""
260 261 for i in super(filteredchangelogmixin, self).revs(start, stop):
261 262 if i not in self.filteredrevs:
262 263 yield i
263 264
264 265 def _checknofilteredinrevs(self, revs):
265 266 """raise the appropriate error if 'revs' contains a filtered revision
266 267
267 268 This returns a version of 'revs' to be used thereafter by the caller.
268 269 In particular, if revs is an iterator, it is converted into a set.
269 270 """
270 271 safehasattr = util.safehasattr
271 272 if safehasattr(revs, '__next__'):
272 273 # Note that inspect.isgenerator() is not true for iterators,
273 274 revs = set(revs)
274 275
275 276 filteredrevs = self.filteredrevs
276 277 if safehasattr(revs, 'first'): # smartset
277 278 offenders = revs & filteredrevs
278 279 else:
279 280 offenders = filteredrevs.intersection(revs)
280 281
281 282 for rev in offenders:
282 283 raise error.FilteredIndexError(rev)
283 284 return revs
284 285
285 286 def headrevs(self, revs=None):
286 287 if revs is None:
287 288 try:
288 289 return self.index.headrevsfiltered(self.filteredrevs)
289 290 # AttributeError covers non-c-extension environments and
290 291 # old c extensions without filter handling.
291 292 except AttributeError:
292 293 return self._headrevs()
293 294
294 295 revs = self._checknofilteredinrevs(revs)
295 296 return super(filteredchangelogmixin, self).headrevs(revs)
296 297
297 298 def strip(self, *args, **kwargs):
298 299 # XXX make something better than assert
299 300 # We can't expect proper strip behavior if we are filtered.
300 301 assert not self.filteredrevs
301 302 super(filteredchangelogmixin, self).strip(*args, **kwargs)
302 303
303 304 def rev(self, node):
304 305 """filtered version of revlog.rev"""
305 306 r = super(filteredchangelogmixin, self).rev(node)
306 307 if r in self.filteredrevs:
307 308 raise error.FilteredLookupError(
308 309 hex(node), self.indexfile, _(b'filtered node')
309 310 )
310 311 return r
311 312
312 313 def node(self, rev):
313 314 """filtered version of revlog.node"""
314 315 if rev in self.filteredrevs:
315 316 raise error.FilteredIndexError(rev)
316 317 return super(filteredchangelogmixin, self).node(rev)
317 318
318 319 def linkrev(self, rev):
319 320 """filtered version of revlog.linkrev"""
320 321 if rev in self.filteredrevs:
321 322 raise error.FilteredIndexError(rev)
322 323 return super(filteredchangelogmixin, self).linkrev(rev)
323 324
324 325 def parentrevs(self, rev):
325 326 """filtered version of revlog.parentrevs"""
326 327 if rev in self.filteredrevs:
327 328 raise error.FilteredIndexError(rev)
328 329 return super(filteredchangelogmixin, self).parentrevs(rev)
329 330
330 331 def flags(self, rev):
331 332 """filtered version of revlog.flags"""
332 333 if rev in self.filteredrevs:
333 334 raise error.FilteredIndexError(rev)
334 335 return super(filteredchangelogmixin, self).flags(rev)
335 336
336 337
337 338 class repoview(object):
338 339 """Provide a read/write view of a repo through a filtered changelog
339 340
340 341 This object is used to access a filtered version of a repository without
341 342 altering the original repository object itself. We can not alter the
342 343 original object for two main reasons:
343 344 - It prevents the use of a repo with multiple filters at the same time. In
344 345 particular when multiple threads are involved.
345 346 - It makes scope of the filtering harder to control.
346 347
347 348 This object behaves very closely to the original repository. All attribute
348 349 operations are done on the original repository:
349 350 - An access to `repoview.someattr` actually returns `repo.someattr`,
350 351 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
351 352 - A deletion of `repoview.someattr` actually drops `someattr`
352 353 from `repo.__dict__`.
353 354
354 355 The only exception is the `changelog` property. It is overridden to return
355 356 a (surface) copy of `repo.changelog` with some revisions filtered. The
356 357 `filtername` attribute of the view control the revisions that need to be
357 358 filtered. (the fact the changelog is copied is an implementation detail).
358 359
359 360 Unlike attributes, this object intercepts all method calls. This means that
360 361 all methods are run on the `repoview` object with the filtered `changelog`
361 362 property. For this purpose the simple `repoview` class must be mixed with
362 363 the actual class of the repository. This ensures that the resulting
363 364 `repoview` object have the very same methods than the repo object. This
364 365 leads to the property below.
365 366
366 367 repoview.method() --> repo.__class__.method(repoview)
367 368
368 369 The inheritance has to be done dynamically because `repo` can be of any
369 370 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
370 371 """
371 372
372 373 def __init__(self, repo, filtername, visibilityexceptions=None):
373 374 object.__setattr__(self, '_unfilteredrepo', repo)
374 375 object.__setattr__(self, 'filtername', filtername)
375 376 object.__setattr__(self, '_clcachekey', None)
376 377 object.__setattr__(self, '_clcache', None)
377 378 # revs which are exceptions and must not be hidden
378 379 object.__setattr__(self, '_visibilityexceptions', visibilityexceptions)
379 380
380 381 # not a propertycache on purpose we shall implement a proper cache later
381 382 @property
382 383 def changelog(self):
383 384 """return a filtered version of the changeset
384 385
385 386 this changelog must not be used for writing"""
386 387 # some cache may be implemented later
387 388 unfi = self._unfilteredrepo
388 389 unfichangelog = unfi.changelog
389 390 # bypass call to changelog.method
390 391 unfiindex = unfichangelog.index
391 392 unfilen = len(unfiindex)
392 393 unfinode = unfiindex[unfilen - 1][7]
393 394 with util.timedcm('repo filter for %s', self.filtername):
394 395 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
395 396 cl = self._clcache
396 397 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
397 398 # if cl.index is not unfiindex, unfi.changelog would be
398 399 # recreated, and our clcache refers to garbage object
399 400 if cl is not None and (
400 401 cl.index is not unfiindex or newkey != self._clcachekey
401 402 ):
402 403 cl = None
403 404 # could have been made None by the previous if
404 405 if cl is None:
405 406 # Only filter if there's something to filter
406 407 cl = wrapchangelog(unfichangelog, revs) if revs else unfichangelog
407 408 object.__setattr__(self, '_clcache', cl)
408 409 object.__setattr__(self, '_clcachekey', newkey)
409 410 return cl
410 411
411 412 def unfiltered(self):
412 413 """Return an unfiltered version of a repo"""
413 414 return self._unfilteredrepo
414 415
415 416 def filtered(self, name, visibilityexceptions=None):
416 417 """Return a filtered version of a repository"""
417 418 if name == self.filtername and not visibilityexceptions:
418 419 return self
419 420 return self.unfiltered().filtered(name, visibilityexceptions)
420 421
421 422 def __repr__(self):
422 423 return '<%s:%s %r>' % (
423 424 self.__class__.__name__,
424 425 pycompat.sysstr(self.filtername),
425 426 self.unfiltered(),
426 427 )
427 428
428 429 # everything access are forwarded to the proxied repo
429 430 def __getattr__(self, attr):
430 431 return getattr(self._unfilteredrepo, attr)
431 432
432 433 def __setattr__(self, attr, value):
433 434 return setattr(self._unfilteredrepo, attr, value)
434 435
435 436 def __delattr__(self, attr):
436 437 return delattr(self._unfilteredrepo, attr)
437 438
438 439
439 440 # Python <3.4 easily leaks types via __mro__. See
440 441 # https://bugs.python.org/issue17950. We cache dynamically created types
441 442 # so they won't be leaked on every invocation of repo.filtered().
442 443 _filteredrepotypes = weakref.WeakKeyDictionary()
443 444
444 445
445 446 def newtype(base):
446 447 """Create a new type with the repoview mixin and the given base class"""
447 448 if base not in _filteredrepotypes:
448 449
449 450 class filteredrepo(repoview, base):
450 451 pass
451 452
452 453 _filteredrepotypes[base] = filteredrepo
453 454 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now