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