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