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