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