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