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