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