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