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