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