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