##// END OF EJS Templates
repoview: cache filtered changelog...
Pierre-Yves David -
r18445:4d92e2d7 default
parent child Browse files
Show More
@@ -1,196 +1,219 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 import copy
9 import copy
10 import phases
10 import phases
11 import util
11 import util
12 import obsolete, bookmarks, revset
12 import obsolete, bookmarks, revset
13
13
14
14
15 def hideablerevs(repo):
15 def hideablerevs(repo):
16 """Revisions candidates to be hidden
16 """Revisions candidates to be hidden
17
17
18 This is a standalone function to help extensions to wrap it."""
18 This is a standalone function to help extensions to wrap it."""
19 return obsolete.getrevs(repo, 'obsolete')
19 return obsolete.getrevs(repo, 'obsolete')
20
20
21 def computehidden(repo):
21 def computehidden(repo):
22 """compute the set of hidden revision to filter
22 """compute the set of hidden revision to filter
23
23
24 During most operation hidden should be filtered."""
24 During most operation hidden should be filtered."""
25 assert not repo.changelog.filteredrevs
25 assert not repo.changelog.filteredrevs
26 hideable = hideablerevs(repo)
26 hideable = hideablerevs(repo)
27 if hideable:
27 if hideable:
28 cl = repo.changelog
28 cl = repo.changelog
29 firsthideable = min(hideable)
29 firsthideable = min(hideable)
30 revs = cl.revs(start=firsthideable)
30 revs = cl.revs(start=firsthideable)
31 blockers = [r for r in revset._children(repo, revs, hideable)
31 blockers = [r for r in revset._children(repo, revs, hideable)
32 if r not in hideable]
32 if r not in hideable]
33 for par in repo[None].parents():
33 for par in repo[None].parents():
34 blockers.append(par.rev())
34 blockers.append(par.rev())
35 for bm in bookmarks.listbookmarks(repo).values():
35 for bm in bookmarks.listbookmarks(repo).values():
36 blockers.append(repo[bm].rev())
36 blockers.append(repo[bm].rev())
37 blocked = cl.ancestors(blockers, inclusive=True)
37 blocked = cl.ancestors(blockers, inclusive=True)
38 return frozenset(r for r in hideable if r not in blocked)
38 return frozenset(r for r in hideable if r not in blocked)
39 return frozenset()
39 return frozenset()
40
40
41 def computeunserved(repo):
41 def computeunserved(repo):
42 """compute the set of revision that should be filtered when used a server
42 """compute the set of revision that should be filtered when used a server
43
43
44 Secret and hidden changeset should not pretend to be here."""
44 Secret and hidden changeset should not pretend to be here."""
45 assert not repo.changelog.filteredrevs
45 assert not repo.changelog.filteredrevs
46 # fast path in simple case to avoid impact of non optimised code
46 # fast path in simple case to avoid impact of non optimised code
47 hiddens = filterrevs(repo, 'visible')
47 hiddens = filterrevs(repo, 'visible')
48 if phases.hassecret(repo):
48 if phases.hassecret(repo):
49 cl = repo.changelog
49 cl = repo.changelog
50 secret = phases.secret
50 secret = phases.secret
51 getphase = repo._phasecache.phase
51 getphase = repo._phasecache.phase
52 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
52 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
53 revs = cl.revs(start=first)
53 revs = cl.revs(start=first)
54 secrets = set(r for r in revs if getphase(repo, r) >= secret)
54 secrets = set(r for r in revs if getphase(repo, r) >= secret)
55 return frozenset(hiddens | secrets)
55 return frozenset(hiddens | secrets)
56 else:
56 else:
57 return hiddens
57 return hiddens
58 return frozenset()
58 return frozenset()
59
59
60 def computemutable(repo):
60 def computemutable(repo):
61 """compute the set of revision that should be filtered when used a server
61 """compute the set of revision that should be filtered when used a server
62
62
63 Secret and hidden changeset should not pretend to be here."""
63 Secret and hidden changeset should not pretend to be here."""
64 assert not repo.changelog.filteredrevs
64 assert not repo.changelog.filteredrevs
65 # fast check to avoid revset call on huge repo
65 # fast check to avoid revset call on huge repo
66 if util.any(repo._phasecache.phaseroots[1:]):
66 if util.any(repo._phasecache.phaseroots[1:]):
67 getphase = repo._phasecache.phase
67 getphase = repo._phasecache.phase
68 maymutable = filterrevs(repo, 'base')
68 maymutable = filterrevs(repo, 'base')
69 return frozenset(r for r in maymutable if getphase(repo, r))
69 return frozenset(r for r in maymutable if getphase(repo, r))
70 return frozenset()
70 return frozenset()
71
71
72 def computeimpactable(repo):
72 def computeimpactable(repo):
73 """Everything impactable by mutable revision
73 """Everything impactable by mutable revision
74
74
75 The mutable filter still have some chance to get invalidated. This will
75 The mutable filter still have some chance to get invalidated. This will
76 happen when:
76 happen when:
77
77
78 - you garbage collect hidden changeset,
78 - you garbage collect hidden changeset,
79 - public phase is moved backward,
79 - public phase is moved backward,
80 - something is changed in the filtering (this could be fixed)
80 - something is changed in the filtering (this could be fixed)
81
81
82 This filter out any mutable changeset and any public changeset that may be
82 This filter out any mutable changeset and any public changeset that may be
83 impacted by something happening to a mutable revision.
83 impacted by something happening to a mutable revision.
84
84
85 This is achieved by filtered everything with a revision number egal or
85 This is achieved by filtered everything with a revision number egal or
86 higher than the first mutable changeset is filtered."""
86 higher than the first mutable changeset is filtered."""
87 assert not repo.changelog.filteredrevs
87 assert not repo.changelog.filteredrevs
88 cl = repo.changelog
88 cl = repo.changelog
89 firstmutable = len(cl)
89 firstmutable = len(cl)
90 for roots in repo._phasecache.phaseroots[1:]:
90 for roots in repo._phasecache.phaseroots[1:]:
91 if roots:
91 if roots:
92 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
92 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
93 # protect from nullrev root
93 # protect from nullrev root
94 firstmutable = max(0, firstmutable)
94 firstmutable = max(0, firstmutable)
95 return frozenset(xrange(firstmutable, len(cl)))
95 return frozenset(xrange(firstmutable, len(cl)))
96
96
97 # function to compute filtered set
97 # function to compute filtered set
98 filtertable = {'visible': computehidden,
98 filtertable = {'visible': computehidden,
99 'served': computeunserved,
99 'served': computeunserved,
100 'immutable': computemutable,
100 'immutable': computemutable,
101 'base': computeimpactable}
101 'base': computeimpactable}
102 ### Nearest subset relation
102 ### Nearest subset relation
103 # Nearest subset of filter X is a filter Y so that:
103 # Nearest subset of filter X is a filter Y so that:
104 # * Y is included in X,
104 # * Y is included in X,
105 # * X - Y is as small as possible.
105 # * X - Y is as small as possible.
106 # This create and ordering used for branchmap purpose.
106 # This create and ordering used for branchmap purpose.
107 # the ordering may be partial
107 # the ordering may be partial
108 subsettable = {None: 'visible',
108 subsettable = {None: 'visible',
109 'visible': 'served',
109 'visible': 'served',
110 'served': 'immutable',
110 'served': 'immutable',
111 'immutable': 'base'}
111 'immutable': 'base'}
112
112
113 def filterrevs(repo, filtername):
113 def filterrevs(repo, filtername):
114 """returns set of filtered revision for this filter name"""
114 """returns set of filtered revision for this filter name"""
115 if filtername not in repo.filteredrevcache:
115 if filtername not in repo.filteredrevcache:
116 func = filtertable[filtername]
116 func = filtertable[filtername]
117 repo.filteredrevcache[filtername] = func(repo.unfiltered())
117 repo.filteredrevcache[filtername] = func(repo.unfiltered())
118 return repo.filteredrevcache[filtername]
118 return repo.filteredrevcache[filtername]
119
119
120 class repoview(object):
120 class repoview(object):
121 """Provide a read/write view of a repo through a filtered changelog
121 """Provide a read/write view of a repo through a filtered changelog
122
122
123 This object is used to access a filtered version of a repository without
123 This object is used to access a filtered version of a repository without
124 altering the original repository object itself. We can not alter the
124 altering the original repository object itself. We can not alter the
125 original object for two main reasons:
125 original object for two main reasons:
126 - It prevents the use of a repo with multiple filters at the same time. In
126 - It prevents the use of a repo with multiple filters at the same time. In
127 particular when multiple threads are involved.
127 particular when multiple threads are involved.
128 - It makes scope of the filtering harder to control.
128 - It makes scope of the filtering harder to control.
129
129
130 This object behaves very closely to the original repository. All attribute
130 This object behaves very closely to the original repository. All attribute
131 operations are done on the original repository:
131 operations are done on the original repository:
132 - An access to `repoview.someattr` actually returns `repo.someattr`,
132 - An access to `repoview.someattr` actually returns `repo.someattr`,
133 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
133 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
134 - A deletion of `repoview.someattr` actually drops `someattr`
134 - A deletion of `repoview.someattr` actually drops `someattr`
135 from `repo.__dict__`.
135 from `repo.__dict__`.
136
136
137 The only exception is the `changelog` property. It is overridden to return
137 The only exception is the `changelog` property. It is overridden to return
138 a (surface) copy of `repo.changelog` with some revisions filtered. The
138 a (surface) copy of `repo.changelog` with some revisions filtered. The
139 `filtername` attribute of the view control the revisions that need to be
139 `filtername` attribute of the view control the revisions that need to be
140 filtered. (the fact the changelog is copied is an implementation detail).
140 filtered. (the fact the changelog is copied is an implementation detail).
141
141
142 Unlike attributes, this object intercepts all method calls. This means that
142 Unlike attributes, this object intercepts all method calls. This means that
143 all methods are run on the `repoview` object with the filtered `changelog`
143 all methods are run on the `repoview` object with the filtered `changelog`
144 property. For this purpose the simple `repoview` class must be mixed with
144 property. For this purpose the simple `repoview` class must be mixed with
145 the actual class of the repository. This ensures that the resulting
145 the actual class of the repository. This ensures that the resulting
146 `repoview` object have the very same methods than the repo object. This
146 `repoview` object have the very same methods than the repo object. This
147 leads to the property below.
147 leads to the property below.
148
148
149 repoview.method() --> repo.__class__.method(repoview)
149 repoview.method() --> repo.__class__.method(repoview)
150
150
151 The inheritance has to be done dynamically because `repo` can be of any
151 The inheritance has to be done dynamically because `repo` can be of any
152 subclasses of `localrepo`. Eg: `bundlerepo` or `httprepo`.
152 subclasses of `localrepo`. Eg: `bundlerepo` or `httprepo`.
153 """
153 """
154
154
155 def __init__(self, repo, filtername):
155 def __init__(self, repo, filtername):
156 object.__setattr__(self, '_unfilteredrepo', repo)
156 object.__setattr__(self, '_unfilteredrepo', repo)
157 object.__setattr__(self, 'filtername', filtername)
157 object.__setattr__(self, 'filtername', filtername)
158 object.__setattr__(self, '_clcachekey', None)
159 object.__setattr__(self, '_clcache', None)
158
160
159 # not a cacheproperty on purpose we shall implement a proper cache later
161 # not a cacheproperty on purpose we shall implement a proper cache later
160 @property
162 @property
161 def changelog(self):
163 def changelog(self):
162 """return a filtered version of the changeset
164 """return a filtered version of the changeset
163
165
164 this changelog must not be used for writing"""
166 this changelog must not be used for writing"""
165 # some cache may be implemented later
167 # some cache may be implemented later
166 cl = copy.copy(self._unfilteredrepo.changelog)
168 unfi = self._unfilteredrepo
167 cl.filteredrevs = filterrevs(self._unfilteredrepo, self.filtername)
169 unfichangelog = unfi.changelog
170 revs = filterrevs(unfi, self.filtername)
171 cl = self._clcache
172 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
173 if cl is not None:
174 # we need to check curkey too for some obscure reason.
175 # MQ test show a corruption of the underlying repo (in _clcache)
176 # without change in the cachekey.
177 oldfilter = cl.filteredrevs
178 try:
179 cl.filterrevs = () # disable filtering for tip
180 curkey = (len(cl), cl.tip(), hash(oldfilter))
181 finally:
182 cl.filteredrevs = oldfilter
183 if newkey != self._clcachekey or newkey != curkey:
184 cl = None
185 # could have been made None by the previous if
186 if cl is None:
187 cl = copy.copy(unfichangelog)
188 cl.filteredrevs = revs
189 object.__setattr__(self, '_clcache', cl)
190 object.__setattr__(self, '_clcachekey', newkey)
168 return cl
191 return cl
169
192
170 def unfiltered(self):
193 def unfiltered(self):
171 """Return an unfiltered version of a repo"""
194 """Return an unfiltered version of a repo"""
172 return self._unfilteredrepo
195 return self._unfilteredrepo
173
196
174 def filtered(self, name):
197 def filtered(self, name):
175 """Return a filtered version of a repository"""
198 """Return a filtered version of a repository"""
176 if name == self.filtername:
199 if name == self.filtername:
177 return self
200 return self
178 return self.unfiltered().filtered(name)
201 return self.unfiltered().filtered(name)
179
202
180 # everything access are forwarded to the proxied repo
203 # everything access are forwarded to the proxied repo
181 def __getattr__(self, attr):
204 def __getattr__(self, attr):
182 return getattr(self._unfilteredrepo, attr)
205 return getattr(self._unfilteredrepo, attr)
183
206
184 def __setattr__(self, attr, value):
207 def __setattr__(self, attr, value):
185 return setattr(self._unfilteredrepo, attr, value)
208 return setattr(self._unfilteredrepo, attr, value)
186
209
187 def __delattr__(self, attr):
210 def __delattr__(self, attr):
188 return delattr(self._unfilteredrepo, attr)
211 return delattr(self._unfilteredrepo, attr)
189
212
190 # The `requirement` attribut is initialiazed during __init__. But
213 # The `requirement` attribut is initialiazed during __init__. But
191 # __getattr__ won't be called as it also exists on the class. We need
214 # __getattr__ won't be called as it also exists on the class. We need
192 # explicit forwarding to main repo here
215 # explicit forwarding to main repo here
193 @property
216 @property
194 def requirements(self):
217 def requirements(self):
195 return self._unfilteredrepo.requirements
218 return self._unfilteredrepo.requirements
196
219
General Comments 0
You need to be logged in to leave comments. Login now