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