##// END OF EJS Templates
repoview: add caching bits...
David Soria Parra -
r22150:45b5cd94 default
parent child Browse files
Show More
@@ -1,250 +1,317 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 error
11 import hashlib
10 import phases
12 import phases
11 import util
13 import util
12 import obsolete
14 import obsolete
15 import struct
13 import tags as tagsmod
16 import tags as tagsmod
14
17 from mercurial.i18n import _
15
18
16 def hideablerevs(repo):
19 def hideablerevs(repo):
17 """Revisions candidates to be hidden
20 """Revisions candidates to be hidden
18
21
19 This is a standalone function to help extensions to wrap it."""
22 This is a standalone function to help extensions to wrap it."""
20 return obsolete.getrevs(repo, 'obsolete')
23 return obsolete.getrevs(repo, 'obsolete')
21
24
22 def _getstaticblockers(repo):
25 def _getstaticblockers(repo):
23 """Cacheable revisions blocking hidden changesets from being filtered.
26 """Cacheable revisions blocking hidden changesets from being filtered.
24
27
25 Additional non-cached hidden blockers are computed in _getdynamicblockers.
28 Additional non-cached hidden blockers are computed in _getdynamicblockers.
26 This is a standalone function to help extensions to wrap it."""
29 This is a standalone function to help extensions to wrap it."""
27 assert not repo.changelog.filteredrevs
30 assert not repo.changelog.filteredrevs
28 hideable = hideablerevs(repo)
31 hideable = hideablerevs(repo)
29 blockers = set()
32 blockers = set()
30 if hideable:
33 if hideable:
31 # We use cl to avoid recursive lookup from repo[xxx]
34 # We use cl to avoid recursive lookup from repo[xxx]
32 cl = repo.changelog
35 cl = repo.changelog
33 firsthideable = min(hideable)
36 firsthideable = min(hideable)
34 revs = cl.revs(start=firsthideable)
37 revs = cl.revs(start=firsthideable)
35 tofilter = repo.revs(
38 tofilter = repo.revs(
36 '(%ld) and children(%ld)', list(revs), list(hideable))
39 '(%ld) and children(%ld)', list(revs), list(hideable))
37 blockers.update([r for r in tofilter if r not in hideable])
40 blockers.update([r for r in tofilter if r not in hideable])
38 return blockers
41 return blockers
39
42
40 def _getdynamicblockers(repo):
43 def _getdynamicblockers(repo):
41 """Non-cacheable revisions blocking hidden changesets from being filtered.
44 """Non-cacheable revisions blocking hidden changesets from being filtered.
42
45
43 Get revisions that will block hidden changesets and are likely to change,
46 Get revisions that will block hidden changesets and are likely to change,
44 but unlikely to create hidden blockers. They won't be cached, so be careful
47 but unlikely to create hidden blockers. They won't be cached, so be careful
45 with adding additional computation."""
48 with adding additional computation."""
46
49
47 cl = repo.changelog
50 cl = repo.changelog
48 blockers = set()
51 blockers = set()
49 blockers.update([par.rev() for par in repo[None].parents()])
52 blockers.update([par.rev() for par in repo[None].parents()])
50 blockers.update([cl.rev(bm) for bm in repo._bookmarks.values()])
53 blockers.update([cl.rev(bm) for bm in repo._bookmarks.values()])
51
54
52 tags = {}
55 tags = {}
53 tagsmod.readlocaltags(repo.ui, repo, tags, {})
56 tagsmod.readlocaltags(repo.ui, repo, tags, {})
54 if tags:
57 if tags:
55 rev, nodemap = cl.rev, cl.nodemap
58 rev, nodemap = cl.rev, cl.nodemap
56 blockers.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
59 blockers.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
57 return blockers
60 return blockers
58
61
62 cacheversion = 1
63 cachefile = 'cache/hidden'
64
65 def cachehash(repo, hideable):
66 """return sha1 hash of repository data to identify a valid cache.
67
68 We calculate a sha1 of repo heads and the content of the obsstore and write
69 it to the cache. Upon reading we can easily validate by checking the hash
70 against the stored one and discard the cache in case the hashes don't match.
71 """
72 h = hashlib.sha1()
73 h.update(''.join(repo.heads()))
74 h.update(str(hash(frozenset(hideable))))
75 return h.digest()
76
77 def trywritehiddencache(repo, hideable, hidden):
78 """write cache of hidden changesets to disk
79
80 Will not write the cache if a wlock cannot be obtained lazily.
81 The cache consists of a head of 22byte:
82 2 byte version number of the cache
83 20 byte sha1 to validate the cache
84 n*4 byte hidden revs
85 """
86 wlock = fh = None
87 try:
88 wlock = repo.wlock(wait=False)
89 # write cache to file
90 newhash = cachehash(repo, hideable)
91 sortedset = sorted(hidden)
92 data = struct.pack('>%iI' % len(sortedset), *sortedset)
93 fh = repo.vfs.open(cachefile, 'w+b', atomictemp=True)
94 fh.write(struct.pack(">H", cacheversion))
95 fh.write(newhash)
96 fh.write(data)
97 except (IOError, OSError):
98 ui.debug('error writing hidden changesets cache')
99 except error.LockHeld:
100 ui.debug('cannot obtain lock to write hidden changesets cache')
101 finally:
102 if fh:
103 fh.close()
104 if wlock:
105 wlock.release()
106
107 def tryreadcache(repo, hideable):
108 """read a cache if the cache exists and is valid, otherwise returns None."""
109 hidden = fh = None
110 try:
111 if repo.vfs.exists(cachefile):
112 fh = repo.vfs.open(cachefile, 'rb')
113 version, = struct.unpack(">H", fh.read(2))
114 oldhash = fh.read(20)
115 newhash = cachehash(repo, hideable)
116 if (cacheversion, oldhash) == (version, newhash):
117 # cache is valid, so we can start reading the hidden revs
118 data = fh.read()
119 count = len(data) / 4
120 hidden = frozenset(struct.unpack('>%iI' % count, data))
121 return hidden
122 finally:
123 if fh:
124 fh.close()
125
59 def computehidden(repo):
126 def computehidden(repo):
60 """compute the set of hidden revision to filter
127 """compute the set of hidden revision to filter
61
128
62 During most operation hidden should be filtered."""
129 During most operation hidden should be filtered."""
63 assert not repo.changelog.filteredrevs
130 assert not repo.changelog.filteredrevs
64 hidden = frozenset()
131 hidden = frozenset()
65 hideable = hideablerevs(repo)
132 hideable = hideablerevs(repo)
66 if hideable:
133 if hideable:
67 cl = repo.changelog
134 cl = repo.changelog
68 blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True)
135 blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True)
69 hidden = frozenset(r for r in hideable if r not in blocked)
136 hidden = frozenset(r for r in hideable if r not in blocked)
70
137
71 # check if we have wd parents, bookmarks or tags pointing to hidden
138 # check if we have wd parents, bookmarks or tags pointing to hidden
72 # changesets and remove those.
139 # changesets and remove those.
73 dynamic = hidden & _getdynamicblockers(repo)
140 dynamic = hidden & _getdynamicblockers(repo)
74 if dynamic:
141 if dynamic:
75 blocked = cl.ancestors(dynamic, inclusive=True)
142 blocked = cl.ancestors(dynamic, inclusive=True)
76 hidden = frozenset(r for r in hidden if r not in blocked)
143 hidden = frozenset(r for r in hidden if r not in blocked)
77 return hidden
144 return hidden
78
145
79 def computeunserved(repo):
146 def computeunserved(repo):
80 """compute the set of revision that should be filtered when used a server
147 """compute the set of revision that should be filtered when used a server
81
148
82 Secret and hidden changeset should not pretend to be here."""
149 Secret and hidden changeset should not pretend to be here."""
83 assert not repo.changelog.filteredrevs
150 assert not repo.changelog.filteredrevs
84 # fast path in simple case to avoid impact of non optimised code
151 # fast path in simple case to avoid impact of non optimised code
85 hiddens = filterrevs(repo, 'visible')
152 hiddens = filterrevs(repo, 'visible')
86 if phases.hassecret(repo):
153 if phases.hassecret(repo):
87 cl = repo.changelog
154 cl = repo.changelog
88 secret = phases.secret
155 secret = phases.secret
89 getphase = repo._phasecache.phase
156 getphase = repo._phasecache.phase
90 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
157 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
91 revs = cl.revs(start=first)
158 revs = cl.revs(start=first)
92 secrets = set(r for r in revs if getphase(repo, r) >= secret)
159 secrets = set(r for r in revs if getphase(repo, r) >= secret)
93 return frozenset(hiddens | secrets)
160 return frozenset(hiddens | secrets)
94 else:
161 else:
95 return hiddens
162 return hiddens
96
163
97 def computemutable(repo):
164 def computemutable(repo):
98 """compute the set of revision that should be filtered when used a server
165 """compute the set of revision that should be filtered when used a server
99
166
100 Secret and hidden changeset should not pretend to be here."""
167 Secret and hidden changeset should not pretend to be here."""
101 assert not repo.changelog.filteredrevs
168 assert not repo.changelog.filteredrevs
102 # fast check to avoid revset call on huge repo
169 # fast check to avoid revset call on huge repo
103 if util.any(repo._phasecache.phaseroots[1:]):
170 if util.any(repo._phasecache.phaseroots[1:]):
104 getphase = repo._phasecache.phase
171 getphase = repo._phasecache.phase
105 maymutable = filterrevs(repo, 'base')
172 maymutable = filterrevs(repo, 'base')
106 return frozenset(r for r in maymutable if getphase(repo, r))
173 return frozenset(r for r in maymutable if getphase(repo, r))
107 return frozenset()
174 return frozenset()
108
175
109 def computeimpactable(repo):
176 def computeimpactable(repo):
110 """Everything impactable by mutable revision
177 """Everything impactable by mutable revision
111
178
112 The immutable filter still have some chance to get invalidated. This will
179 The immutable filter still have some chance to get invalidated. This will
113 happen when:
180 happen when:
114
181
115 - you garbage collect hidden changeset,
182 - you garbage collect hidden changeset,
116 - public phase is moved backward,
183 - public phase is moved backward,
117 - something is changed in the filtering (this could be fixed)
184 - something is changed in the filtering (this could be fixed)
118
185
119 This filter out any mutable changeset and any public changeset that may be
186 This filter out any mutable changeset and any public changeset that may be
120 impacted by something happening to a mutable revision.
187 impacted by something happening to a mutable revision.
121
188
122 This is achieved by filtered everything with a revision number egal or
189 This is achieved by filtered everything with a revision number egal or
123 higher than the first mutable changeset is filtered."""
190 higher than the first mutable changeset is filtered."""
124 assert not repo.changelog.filteredrevs
191 assert not repo.changelog.filteredrevs
125 cl = repo.changelog
192 cl = repo.changelog
126 firstmutable = len(cl)
193 firstmutable = len(cl)
127 for roots in repo._phasecache.phaseroots[1:]:
194 for roots in repo._phasecache.phaseroots[1:]:
128 if roots:
195 if roots:
129 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
196 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
130 # protect from nullrev root
197 # protect from nullrev root
131 firstmutable = max(0, firstmutable)
198 firstmutable = max(0, firstmutable)
132 return frozenset(xrange(firstmutable, len(cl)))
199 return frozenset(xrange(firstmutable, len(cl)))
133
200
134 # function to compute filtered set
201 # function to compute filtered set
135 #
202 #
136 # When adding a new filter you MUST update the table at:
203 # When adding a new filter you MUST update the table at:
137 # mercurial.branchmap.subsettable
204 # mercurial.branchmap.subsettable
138 # Otherwise your filter will have to recompute all its branches cache
205 # Otherwise your filter will have to recompute all its branches cache
139 # from scratch (very slow).
206 # from scratch (very slow).
140 filtertable = {'visible': computehidden,
207 filtertable = {'visible': computehidden,
141 'served': computeunserved,
208 'served': computeunserved,
142 'immutable': computemutable,
209 'immutable': computemutable,
143 'base': computeimpactable}
210 'base': computeimpactable}
144
211
145 def filterrevs(repo, filtername):
212 def filterrevs(repo, filtername):
146 """returns set of filtered revision for this filter name"""
213 """returns set of filtered revision for this filter name"""
147 if filtername not in repo.filteredrevcache:
214 if filtername not in repo.filteredrevcache:
148 func = filtertable[filtername]
215 func = filtertable[filtername]
149 repo.filteredrevcache[filtername] = func(repo.unfiltered())
216 repo.filteredrevcache[filtername] = func(repo.unfiltered())
150 return repo.filteredrevcache[filtername]
217 return repo.filteredrevcache[filtername]
151
218
152 class repoview(object):
219 class repoview(object):
153 """Provide a read/write view of a repo through a filtered changelog
220 """Provide a read/write view of a repo through a filtered changelog
154
221
155 This object is used to access a filtered version of a repository without
222 This object is used to access a filtered version of a repository without
156 altering the original repository object itself. We can not alter the
223 altering the original repository object itself. We can not alter the
157 original object for two main reasons:
224 original object for two main reasons:
158 - It prevents the use of a repo with multiple filters at the same time. In
225 - It prevents the use of a repo with multiple filters at the same time. In
159 particular when multiple threads are involved.
226 particular when multiple threads are involved.
160 - It makes scope of the filtering harder to control.
227 - It makes scope of the filtering harder to control.
161
228
162 This object behaves very closely to the original repository. All attribute
229 This object behaves very closely to the original repository. All attribute
163 operations are done on the original repository:
230 operations are done on the original repository:
164 - An access to `repoview.someattr` actually returns `repo.someattr`,
231 - An access to `repoview.someattr` actually returns `repo.someattr`,
165 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
232 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
166 - A deletion of `repoview.someattr` actually drops `someattr`
233 - A deletion of `repoview.someattr` actually drops `someattr`
167 from `repo.__dict__`.
234 from `repo.__dict__`.
168
235
169 The only exception is the `changelog` property. It is overridden to return
236 The only exception is the `changelog` property. It is overridden to return
170 a (surface) copy of `repo.changelog` with some revisions filtered. The
237 a (surface) copy of `repo.changelog` with some revisions filtered. The
171 `filtername` attribute of the view control the revisions that need to be
238 `filtername` attribute of the view control the revisions that need to be
172 filtered. (the fact the changelog is copied is an implementation detail).
239 filtered. (the fact the changelog is copied is an implementation detail).
173
240
174 Unlike attributes, this object intercepts all method calls. This means that
241 Unlike attributes, this object intercepts all method calls. This means that
175 all methods are run on the `repoview` object with the filtered `changelog`
242 all methods are run on the `repoview` object with the filtered `changelog`
176 property. For this purpose the simple `repoview` class must be mixed with
243 property. For this purpose the simple `repoview` class must be mixed with
177 the actual class of the repository. This ensures that the resulting
244 the actual class of the repository. This ensures that the resulting
178 `repoview` object have the very same methods than the repo object. This
245 `repoview` object have the very same methods than the repo object. This
179 leads to the property below.
246 leads to the property below.
180
247
181 repoview.method() --> repo.__class__.method(repoview)
248 repoview.method() --> repo.__class__.method(repoview)
182
249
183 The inheritance has to be done dynamically because `repo` can be of any
250 The inheritance has to be done dynamically because `repo` can be of any
184 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
251 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
185 """
252 """
186
253
187 def __init__(self, repo, filtername):
254 def __init__(self, repo, filtername):
188 object.__setattr__(self, '_unfilteredrepo', repo)
255 object.__setattr__(self, '_unfilteredrepo', repo)
189 object.__setattr__(self, 'filtername', filtername)
256 object.__setattr__(self, 'filtername', filtername)
190 object.__setattr__(self, '_clcachekey', None)
257 object.__setattr__(self, '_clcachekey', None)
191 object.__setattr__(self, '_clcache', None)
258 object.__setattr__(self, '_clcache', None)
192
259
193 # not a propertycache on purpose we shall implement a proper cache later
260 # not a propertycache on purpose we shall implement a proper cache later
194 @property
261 @property
195 def changelog(self):
262 def changelog(self):
196 """return a filtered version of the changeset
263 """return a filtered version of the changeset
197
264
198 this changelog must not be used for writing"""
265 this changelog must not be used for writing"""
199 # some cache may be implemented later
266 # some cache may be implemented later
200 unfi = self._unfilteredrepo
267 unfi = self._unfilteredrepo
201 unfichangelog = unfi.changelog
268 unfichangelog = unfi.changelog
202 revs = filterrevs(unfi, self.filtername)
269 revs = filterrevs(unfi, self.filtername)
203 cl = self._clcache
270 cl = self._clcache
204 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
271 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
205 if cl is not None:
272 if cl is not None:
206 # we need to check curkey too for some obscure reason.
273 # we need to check curkey too for some obscure reason.
207 # MQ test show a corruption of the underlying repo (in _clcache)
274 # MQ test show a corruption of the underlying repo (in _clcache)
208 # without change in the cachekey.
275 # without change in the cachekey.
209 oldfilter = cl.filteredrevs
276 oldfilter = cl.filteredrevs
210 try:
277 try:
211 cl.filterrevs = () # disable filtering for tip
278 cl.filterrevs = () # disable filtering for tip
212 curkey = (len(cl), cl.tip(), hash(oldfilter))
279 curkey = (len(cl), cl.tip(), hash(oldfilter))
213 finally:
280 finally:
214 cl.filteredrevs = oldfilter
281 cl.filteredrevs = oldfilter
215 if newkey != self._clcachekey or newkey != curkey:
282 if newkey != self._clcachekey or newkey != curkey:
216 cl = None
283 cl = None
217 # could have been made None by the previous if
284 # could have been made None by the previous if
218 if cl is None:
285 if cl is None:
219 cl = copy.copy(unfichangelog)
286 cl = copy.copy(unfichangelog)
220 cl.filteredrevs = revs
287 cl.filteredrevs = revs
221 object.__setattr__(self, '_clcache', cl)
288 object.__setattr__(self, '_clcache', cl)
222 object.__setattr__(self, '_clcachekey', newkey)
289 object.__setattr__(self, '_clcachekey', newkey)
223 return cl
290 return cl
224
291
225 def unfiltered(self):
292 def unfiltered(self):
226 """Return an unfiltered version of a repo"""
293 """Return an unfiltered version of a repo"""
227 return self._unfilteredrepo
294 return self._unfilteredrepo
228
295
229 def filtered(self, name):
296 def filtered(self, name):
230 """Return a filtered version of a repository"""
297 """Return a filtered version of a repository"""
231 if name == self.filtername:
298 if name == self.filtername:
232 return self
299 return self
233 return self.unfiltered().filtered(name)
300 return self.unfiltered().filtered(name)
234
301
235 # everything access are forwarded to the proxied repo
302 # everything access are forwarded to the proxied repo
236 def __getattr__(self, attr):
303 def __getattr__(self, attr):
237 return getattr(self._unfilteredrepo, attr)
304 return getattr(self._unfilteredrepo, attr)
238
305
239 def __setattr__(self, attr, value):
306 def __setattr__(self, attr, value):
240 return setattr(self._unfilteredrepo, attr, value)
307 return setattr(self._unfilteredrepo, attr, value)
241
308
242 def __delattr__(self, attr):
309 def __delattr__(self, attr):
243 return delattr(self._unfilteredrepo, attr)
310 return delattr(self._unfilteredrepo, attr)
244
311
245 # The `requirements` attribute is initialized during __init__. But
312 # The `requirements` attribute is initialized during __init__. But
246 # __getattr__ won't be called as it also exists on the class. We need
313 # __getattr__ won't be called as it also exists on the class. We need
247 # explicit forwarding to main repo here
314 # explicit forwarding to main repo here
248 @property
315 @property
249 def requirements(self):
316 def requirements(self):
250 return self._unfilteredrepo.requirements
317 return self._unfilteredrepo.requirements
General Comments 0
You need to be logged in to leave comments. Login now