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