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