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