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