##// END OF EJS Templates
repoview: extract a function for wrapping changelog...
Martin von Zweigbergk -
r43746:d630c571 default
parent child Browse files
Show More
@@ -1,337 +1,342 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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import weakref
12 import weakref
13
13
14 from .node import nullrev
14 from .node import nullrev
15 from .pycompat import (
15 from .pycompat import (
16 delattr,
16 delattr,
17 getattr,
17 getattr,
18 setattr,
18 setattr,
19 )
19 )
20 from . import (
20 from . import (
21 obsolete,
21 obsolete,
22 phases,
22 phases,
23 pycompat,
23 pycompat,
24 tags as tagsmod,
24 tags as tagsmod,
25 util,
25 util,
26 )
26 )
27 from .utils import repoviewutil
27 from .utils import repoviewutil
28
28
29
29
30 def hideablerevs(repo):
30 def hideablerevs(repo):
31 """Revision candidates to be hidden
31 """Revision candidates to be hidden
32
32
33 This is a standalone function to allow extensions to wrap it.
33 This is a standalone function to allow extensions to wrap it.
34
34
35 Because we use the set of immutable changesets as a fallback subset in
35 Because we use the set of immutable changesets as a fallback subset in
36 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
36 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
37 "public" changesets as "hideable". Doing so would break multiple code
37 "public" changesets as "hideable". Doing so would break multiple code
38 assertions and lead to crashes."""
38 assertions and lead to crashes."""
39 obsoletes = obsolete.getrevs(repo, b'obsolete')
39 obsoletes = obsolete.getrevs(repo, b'obsolete')
40 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
40 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
41 internals = frozenset(internals)
41 internals = frozenset(internals)
42 return obsoletes | internals
42 return obsoletes | internals
43
43
44
44
45 def pinnedrevs(repo):
45 def pinnedrevs(repo):
46 """revisions blocking hidden changesets from being filtered
46 """revisions blocking hidden changesets from being filtered
47 """
47 """
48
48
49 cl = repo.changelog
49 cl = repo.changelog
50 pinned = set()
50 pinned = set()
51 pinned.update([par.rev() for par in repo[None].parents()])
51 pinned.update([par.rev() for par in repo[None].parents()])
52 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
52 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
53
53
54 tags = {}
54 tags = {}
55 tagsmod.readlocaltags(repo.ui, repo, tags, {})
55 tagsmod.readlocaltags(repo.ui, repo, tags, {})
56 if tags:
56 if tags:
57 rev, nodemap = cl.rev, cl.nodemap
57 rev, nodemap = cl.rev, cl.nodemap
58 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
58 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
59 return pinned
59 return pinned
60
60
61
61
62 def _revealancestors(pfunc, hidden, revs):
62 def _revealancestors(pfunc, hidden, revs):
63 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
63 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
64 from 'hidden'
64 from 'hidden'
65
65
66 - pfunc(r): a funtion returning parent of 'r',
66 - pfunc(r): a funtion returning parent of 'r',
67 - hidden: the (preliminary) hidden revisions, to be updated
67 - hidden: the (preliminary) hidden revisions, to be updated
68 - revs: iterable of revnum,
68 - revs: iterable of revnum,
69
69
70 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
70 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
71 *not* revealed)
71 *not* revealed)
72 """
72 """
73 stack = list(revs)
73 stack = list(revs)
74 while stack:
74 while stack:
75 for p in pfunc(stack.pop()):
75 for p in pfunc(stack.pop()):
76 if p != nullrev and p in hidden:
76 if p != nullrev and p in hidden:
77 hidden.remove(p)
77 hidden.remove(p)
78 stack.append(p)
78 stack.append(p)
79
79
80
80
81 def computehidden(repo, visibilityexceptions=None):
81 def computehidden(repo, visibilityexceptions=None):
82 """compute the set of hidden revision to filter
82 """compute the set of hidden revision to filter
83
83
84 During most operation hidden should be filtered."""
84 During most operation hidden should be filtered."""
85 assert not repo.changelog.filteredrevs
85 assert not repo.changelog.filteredrevs
86
86
87 hidden = hideablerevs(repo)
87 hidden = hideablerevs(repo)
88 if hidden:
88 if hidden:
89 hidden = set(hidden - pinnedrevs(repo))
89 hidden = set(hidden - pinnedrevs(repo))
90 if visibilityexceptions:
90 if visibilityexceptions:
91 hidden -= visibilityexceptions
91 hidden -= visibilityexceptions
92 pfunc = repo.changelog.parentrevs
92 pfunc = repo.changelog.parentrevs
93 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
93 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
94
94
95 visible = mutable - hidden
95 visible = mutable - hidden
96 _revealancestors(pfunc, hidden, visible)
96 _revealancestors(pfunc, hidden, visible)
97 return frozenset(hidden)
97 return frozenset(hidden)
98
98
99
99
100 def computesecret(repo, visibilityexceptions=None):
100 def computesecret(repo, visibilityexceptions=None):
101 """compute the set of revision that can never be exposed through hgweb
101 """compute the set of revision that can never be exposed through hgweb
102
102
103 Changeset in the secret phase (or above) should stay unaccessible."""
103 Changeset in the secret phase (or above) should stay unaccessible."""
104 assert not repo.changelog.filteredrevs
104 assert not repo.changelog.filteredrevs
105 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
105 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
106 return frozenset(secrets)
106 return frozenset(secrets)
107
107
108
108
109 def computeunserved(repo, visibilityexceptions=None):
109 def computeunserved(repo, visibilityexceptions=None):
110 """compute the set of revision that should be filtered when used a server
110 """compute the set of revision that should be filtered when used a server
111
111
112 Secret and hidden changeset should not pretend to be here."""
112 Secret and hidden changeset should not pretend to be here."""
113 assert not repo.changelog.filteredrevs
113 assert not repo.changelog.filteredrevs
114 # fast path in simple case to avoid impact of non optimised code
114 # fast path in simple case to avoid impact of non optimised code
115 hiddens = filterrevs(repo, b'visible')
115 hiddens = filterrevs(repo, b'visible')
116 secrets = filterrevs(repo, b'served.hidden')
116 secrets = filterrevs(repo, b'served.hidden')
117 if secrets:
117 if secrets:
118 return frozenset(hiddens | secrets)
118 return frozenset(hiddens | secrets)
119 else:
119 else:
120 return hiddens
120 return hiddens
121
121
122
122
123 def computemutable(repo, visibilityexceptions=None):
123 def computemutable(repo, visibilityexceptions=None):
124 assert not repo.changelog.filteredrevs
124 assert not repo.changelog.filteredrevs
125 # fast check to avoid revset call on huge repo
125 # fast check to avoid revset call on huge repo
126 if any(repo._phasecache.phaseroots[1:]):
126 if any(repo._phasecache.phaseroots[1:]):
127 getphase = repo._phasecache.phase
127 getphase = repo._phasecache.phase
128 maymutable = filterrevs(repo, b'base')
128 maymutable = filterrevs(repo, b'base')
129 return frozenset(r for r in maymutable if getphase(repo, r))
129 return frozenset(r for r in maymutable if getphase(repo, r))
130 return frozenset()
130 return frozenset()
131
131
132
132
133 def computeimpactable(repo, visibilityexceptions=None):
133 def computeimpactable(repo, visibilityexceptions=None):
134 """Everything impactable by mutable revision
134 """Everything impactable by mutable revision
135
135
136 The immutable filter still have some chance to get invalidated. This will
136 The immutable filter still have some chance to get invalidated. This will
137 happen when:
137 happen when:
138
138
139 - you garbage collect hidden changeset,
139 - you garbage collect hidden changeset,
140 - public phase is moved backward,
140 - public phase is moved backward,
141 - something is changed in the filtering (this could be fixed)
141 - something is changed in the filtering (this could be fixed)
142
142
143 This filter out any mutable changeset and any public changeset that may be
143 This filter out any mutable changeset and any public changeset that may be
144 impacted by something happening to a mutable revision.
144 impacted by something happening to a mutable revision.
145
145
146 This is achieved by filtered everything with a revision number egal or
146 This is achieved by filtered everything with a revision number egal or
147 higher than the first mutable changeset is filtered."""
147 higher than the first mutable changeset is filtered."""
148 assert not repo.changelog.filteredrevs
148 assert not repo.changelog.filteredrevs
149 cl = repo.changelog
149 cl = repo.changelog
150 firstmutable = len(cl)
150 firstmutable = len(cl)
151 for roots in repo._phasecache.phaseroots[1:]:
151 for roots in repo._phasecache.phaseroots[1:]:
152 if roots:
152 if roots:
153 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
153 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
154 # protect from nullrev root
154 # protect from nullrev root
155 firstmutable = max(0, firstmutable)
155 firstmutable = max(0, firstmutable)
156 return frozenset(pycompat.xrange(firstmutable, len(cl)))
156 return frozenset(pycompat.xrange(firstmutable, len(cl)))
157
157
158
158
159 # function to compute filtered set
159 # function to compute filtered set
160 #
160 #
161 # When adding a new filter you MUST update the table at:
161 # When adding a new filter you MUST update the table at:
162 # mercurial.utils.repoviewutil.subsettable
162 # mercurial.utils.repoviewutil.subsettable
163 # Otherwise your filter will have to recompute all its branches cache
163 # Otherwise your filter will have to recompute all its branches cache
164 # from scratch (very slow).
164 # from scratch (very slow).
165 filtertable = {
165 filtertable = {
166 b'visible': computehidden,
166 b'visible': computehidden,
167 b'visible-hidden': computehidden,
167 b'visible-hidden': computehidden,
168 b'served.hidden': computesecret,
168 b'served.hidden': computesecret,
169 b'served': computeunserved,
169 b'served': computeunserved,
170 b'immutable': computemutable,
170 b'immutable': computemutable,
171 b'base': computeimpactable,
171 b'base': computeimpactable,
172 }
172 }
173
173
174 _basefiltername = list(filtertable)
174 _basefiltername = list(filtertable)
175
175
176
176
177 def extrafilter(ui):
177 def extrafilter(ui):
178 """initialize extra filter and return its id
178 """initialize extra filter and return its id
179
179
180 If extra filtering is configured, we make sure the associated filtered view
180 If extra filtering is configured, we make sure the associated filtered view
181 are declared and return the associated id.
181 are declared and return the associated id.
182 """
182 """
183 frevs = ui.config(b'experimental', b'extra-filter-revs')
183 frevs = ui.config(b'experimental', b'extra-filter-revs')
184 if frevs is None:
184 if frevs is None:
185 return None
185 return None
186
186
187 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
187 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
188
188
189 combine = lambda fname: fname + b'%' + fid
189 combine = lambda fname: fname + b'%' + fid
190
190
191 subsettable = repoviewutil.subsettable
191 subsettable = repoviewutil.subsettable
192
192
193 if combine(b'base') not in filtertable:
193 if combine(b'base') not in filtertable:
194 for name in _basefiltername:
194 for name in _basefiltername:
195
195
196 def extrafilteredrevs(repo, *args, **kwargs):
196 def extrafilteredrevs(repo, *args, **kwargs):
197 baserevs = filtertable[name](repo, *args, **kwargs)
197 baserevs = filtertable[name](repo, *args, **kwargs)
198 extrarevs = frozenset(repo.revs(frevs))
198 extrarevs = frozenset(repo.revs(frevs))
199 return baserevs | extrarevs
199 return baserevs | extrarevs
200
200
201 filtertable[combine(name)] = extrafilteredrevs
201 filtertable[combine(name)] = extrafilteredrevs
202 if name in subsettable:
202 if name in subsettable:
203 subsettable[combine(name)] = combine(subsettable[name])
203 subsettable[combine(name)] = combine(subsettable[name])
204 return fid
204 return fid
205
205
206
206
207 def filterrevs(repo, filtername, visibilityexceptions=None):
207 def filterrevs(repo, filtername, visibilityexceptions=None):
208 """returns set of filtered revision for this filter name
208 """returns set of filtered revision for this filter name
209
209
210 visibilityexceptions is a set of revs which must are exceptions for
210 visibilityexceptions is a set of revs which must are exceptions for
211 hidden-state and must be visible. They are dynamic and hence we should not
211 hidden-state and must be visible. They are dynamic and hence we should not
212 cache it's result"""
212 cache it's result"""
213 if filtername not in repo.filteredrevcache:
213 if filtername not in repo.filteredrevcache:
214 func = filtertable[filtername]
214 func = filtertable[filtername]
215 if visibilityexceptions:
215 if visibilityexceptions:
216 return func(repo.unfiltered, visibilityexceptions)
216 return func(repo.unfiltered, visibilityexceptions)
217 repo.filteredrevcache[filtername] = func(repo.unfiltered())
217 repo.filteredrevcache[filtername] = func(repo.unfiltered())
218 return repo.filteredrevcache[filtername]
218 return repo.filteredrevcache[filtername]
219
219
220
220
221 def wrapchangelog(unfichangelog, filteredrevs):
222 cl = copy.copy(unfichangelog)
223 cl.filteredrevs = filteredrevs
224 return cl
225
226
221 class repoview(object):
227 class repoview(object):
222 """Provide a read/write view of a repo through a filtered changelog
228 """Provide a read/write view of a repo through a filtered changelog
223
229
224 This object is used to access a filtered version of a repository without
230 This object is used to access a filtered version of a repository without
225 altering the original repository object itself. We can not alter the
231 altering the original repository object itself. We can not alter the
226 original object for two main reasons:
232 original object for two main reasons:
227 - It prevents the use of a repo with multiple filters at the same time. In
233 - It prevents the use of a repo with multiple filters at the same time. In
228 particular when multiple threads are involved.
234 particular when multiple threads are involved.
229 - It makes scope of the filtering harder to control.
235 - It makes scope of the filtering harder to control.
230
236
231 This object behaves very closely to the original repository. All attribute
237 This object behaves very closely to the original repository. All attribute
232 operations are done on the original repository:
238 operations are done on the original repository:
233 - An access to `repoview.someattr` actually returns `repo.someattr`,
239 - An access to `repoview.someattr` actually returns `repo.someattr`,
234 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
240 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
235 - A deletion of `repoview.someattr` actually drops `someattr`
241 - A deletion of `repoview.someattr` actually drops `someattr`
236 from `repo.__dict__`.
242 from `repo.__dict__`.
237
243
238 The only exception is the `changelog` property. It is overridden to return
244 The only exception is the `changelog` property. It is overridden to return
239 a (surface) copy of `repo.changelog` with some revisions filtered. The
245 a (surface) copy of `repo.changelog` with some revisions filtered. The
240 `filtername` attribute of the view control the revisions that need to be
246 `filtername` attribute of the view control the revisions that need to be
241 filtered. (the fact the changelog is copied is an implementation detail).
247 filtered. (the fact the changelog is copied is an implementation detail).
242
248
243 Unlike attributes, this object intercepts all method calls. This means that
249 Unlike attributes, this object intercepts all method calls. This means that
244 all methods are run on the `repoview` object with the filtered `changelog`
250 all methods are run on the `repoview` object with the filtered `changelog`
245 property. For this purpose the simple `repoview` class must be mixed with
251 property. For this purpose the simple `repoview` class must be mixed with
246 the actual class of the repository. This ensures that the resulting
252 the actual class of the repository. This ensures that the resulting
247 `repoview` object have the very same methods than the repo object. This
253 `repoview` object have the very same methods than the repo object. This
248 leads to the property below.
254 leads to the property below.
249
255
250 repoview.method() --> repo.__class__.method(repoview)
256 repoview.method() --> repo.__class__.method(repoview)
251
257
252 The inheritance has to be done dynamically because `repo` can be of any
258 The inheritance has to be done dynamically because `repo` can be of any
253 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
259 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
254 """
260 """
255
261
256 def __init__(self, repo, filtername, visibilityexceptions=None):
262 def __init__(self, repo, filtername, visibilityexceptions=None):
257 object.__setattr__(self, r'_unfilteredrepo', repo)
263 object.__setattr__(self, r'_unfilteredrepo', repo)
258 object.__setattr__(self, r'filtername', filtername)
264 object.__setattr__(self, r'filtername', filtername)
259 object.__setattr__(self, r'_clcachekey', None)
265 object.__setattr__(self, r'_clcachekey', None)
260 object.__setattr__(self, r'_clcache', None)
266 object.__setattr__(self, r'_clcache', None)
261 # revs which are exceptions and must not be hidden
267 # revs which are exceptions and must not be hidden
262 object.__setattr__(self, r'_visibilityexceptions', visibilityexceptions)
268 object.__setattr__(self, r'_visibilityexceptions', visibilityexceptions)
263
269
264 # not a propertycache on purpose we shall implement a proper cache later
270 # not a propertycache on purpose we shall implement a proper cache later
265 @property
271 @property
266 def changelog(self):
272 def changelog(self):
267 """return a filtered version of the changeset
273 """return a filtered version of the changeset
268
274
269 this changelog must not be used for writing"""
275 this changelog must not be used for writing"""
270 # some cache may be implemented later
276 # some cache may be implemented later
271 unfi = self._unfilteredrepo
277 unfi = self._unfilteredrepo
272 unfichangelog = unfi.changelog
278 unfichangelog = unfi.changelog
273 # bypass call to changelog.method
279 # bypass call to changelog.method
274 unfiindex = unfichangelog.index
280 unfiindex = unfichangelog.index
275 unfilen = len(unfiindex)
281 unfilen = len(unfiindex)
276 unfinode = unfiindex[unfilen - 1][7]
282 unfinode = unfiindex[unfilen - 1][7]
277 with util.timedcm('repo filter for %s', self.filtername):
283 with util.timedcm('repo filter for %s', self.filtername):
278 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
284 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
279 cl = self._clcache
285 cl = self._clcache
280 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
286 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
281 # if cl.index is not unfiindex, unfi.changelog would be
287 # if cl.index is not unfiindex, unfi.changelog would be
282 # recreated, and our clcache refers to garbage object
288 # recreated, and our clcache refers to garbage object
283 if cl is not None and (
289 if cl is not None and (
284 cl.index is not unfiindex or newkey != self._clcachekey
290 cl.index is not unfiindex or newkey != self._clcachekey
285 ):
291 ):
286 cl = None
292 cl = None
287 # could have been made None by the previous if
293 # could have been made None by the previous if
288 if cl is None:
294 if cl is None:
289 cl = copy.copy(unfichangelog)
295 cl = wrapchangelog(unfichangelog, revs)
290 cl.filteredrevs = revs
291 object.__setattr__(self, r'_clcache', cl)
296 object.__setattr__(self, r'_clcache', cl)
292 object.__setattr__(self, r'_clcachekey', newkey)
297 object.__setattr__(self, r'_clcachekey', newkey)
293 return cl
298 return cl
294
299
295 def unfiltered(self):
300 def unfiltered(self):
296 """Return an unfiltered version of a repo"""
301 """Return an unfiltered version of a repo"""
297 return self._unfilteredrepo
302 return self._unfilteredrepo
298
303
299 def filtered(self, name, visibilityexceptions=None):
304 def filtered(self, name, visibilityexceptions=None):
300 """Return a filtered version of a repository"""
305 """Return a filtered version of a repository"""
301 if name == self.filtername and not visibilityexceptions:
306 if name == self.filtername and not visibilityexceptions:
302 return self
307 return self
303 return self.unfiltered().filtered(name, visibilityexceptions)
308 return self.unfiltered().filtered(name, visibilityexceptions)
304
309
305 def __repr__(self):
310 def __repr__(self):
306 return r'<%s:%s %r>' % (
311 return r'<%s:%s %r>' % (
307 self.__class__.__name__,
312 self.__class__.__name__,
308 pycompat.sysstr(self.filtername),
313 pycompat.sysstr(self.filtername),
309 self.unfiltered(),
314 self.unfiltered(),
310 )
315 )
311
316
312 # everything access are forwarded to the proxied repo
317 # everything access are forwarded to the proxied repo
313 def __getattr__(self, attr):
318 def __getattr__(self, attr):
314 return getattr(self._unfilteredrepo, attr)
319 return getattr(self._unfilteredrepo, attr)
315
320
316 def __setattr__(self, attr, value):
321 def __setattr__(self, attr, value):
317 return setattr(self._unfilteredrepo, attr, value)
322 return setattr(self._unfilteredrepo, attr, value)
318
323
319 def __delattr__(self, attr):
324 def __delattr__(self, attr):
320 return delattr(self._unfilteredrepo, attr)
325 return delattr(self._unfilteredrepo, attr)
321
326
322
327
323 # Python <3.4 easily leaks types via __mro__. See
328 # Python <3.4 easily leaks types via __mro__. See
324 # https://bugs.python.org/issue17950. We cache dynamically created types
329 # https://bugs.python.org/issue17950. We cache dynamically created types
325 # so they won't be leaked on every invocation of repo.filtered().
330 # so they won't be leaked on every invocation of repo.filtered().
326 _filteredrepotypes = weakref.WeakKeyDictionary()
331 _filteredrepotypes = weakref.WeakKeyDictionary()
327
332
328
333
329 def newtype(base):
334 def newtype(base):
330 """Create a new type with the repoview mixin and the given base class"""
335 """Create a new type with the repoview mixin and the given base class"""
331 if base not in _filteredrepotypes:
336 if base not in _filteredrepotypes:
332
337
333 class filteredrepo(repoview, base):
338 class filteredrepo(repoview, base):
334 pass
339 pass
335
340
336 _filteredrepotypes[base] = filteredrepo
341 _filteredrepotypes[base] = filteredrepo
337 return _filteredrepotypes[base]
342 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now