##// END OF EJS Templates
branchcache: don't verify closed nodes in _branchtip()...
Pulkit Goyal -
r42292:be5eeaf5 default
parent child Browse files
Show More
@@ -1,669 +1,668 b''
1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import struct
10 import struct
11
11
12 from .node import (
12 from .node import (
13 bin,
13 bin,
14 hex,
14 hex,
15 nullid,
15 nullid,
16 nullrev,
16 nullrev,
17 )
17 )
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 pycompat,
21 pycompat,
22 scmutil,
22 scmutil,
23 util,
23 util,
24 )
24 )
25 from .utils import (
25 from .utils import (
26 stringutil,
26 stringutil,
27 )
27 )
28
28
29 calcsize = struct.calcsize
29 calcsize = struct.calcsize
30 pack_into = struct.pack_into
30 pack_into = struct.pack_into
31 unpack_from = struct.unpack_from
31 unpack_from = struct.unpack_from
32
32
33
33
34 ### Nearest subset relation
34 ### Nearest subset relation
35 # Nearest subset of filter X is a filter Y so that:
35 # Nearest subset of filter X is a filter Y so that:
36 # * Y is included in X,
36 # * Y is included in X,
37 # * X - Y is as small as possible.
37 # * X - Y is as small as possible.
38 # This create and ordering used for branchmap purpose.
38 # This create and ordering used for branchmap purpose.
39 # the ordering may be partial
39 # the ordering may be partial
40 subsettable = {None: 'visible',
40 subsettable = {None: 'visible',
41 'visible-hidden': 'visible',
41 'visible-hidden': 'visible',
42 'visible': 'served',
42 'visible': 'served',
43 'served': 'immutable',
43 'served': 'immutable',
44 'immutable': 'base'}
44 'immutable': 'base'}
45
45
46
46
47 class BranchMapCache(object):
47 class BranchMapCache(object):
48 """mapping of filtered views of repo with their branchcache"""
48 """mapping of filtered views of repo with their branchcache"""
49 def __init__(self):
49 def __init__(self):
50 self._per_filter = {}
50 self._per_filter = {}
51
51
52 def __getitem__(self, repo):
52 def __getitem__(self, repo):
53 self.updatecache(repo)
53 self.updatecache(repo)
54 return self._per_filter[repo.filtername]
54 return self._per_filter[repo.filtername]
55
55
56 def updatecache(self, repo):
56 def updatecache(self, repo):
57 """Update the cache for the given filtered view on a repository"""
57 """Update the cache for the given filtered view on a repository"""
58 # This can trigger updates for the caches for subsets of the filtered
58 # This can trigger updates for the caches for subsets of the filtered
59 # view, e.g. when there is no cache for this filtered view or the cache
59 # view, e.g. when there is no cache for this filtered view or the cache
60 # is stale.
60 # is stale.
61
61
62 cl = repo.changelog
62 cl = repo.changelog
63 filtername = repo.filtername
63 filtername = repo.filtername
64 bcache = self._per_filter.get(filtername)
64 bcache = self._per_filter.get(filtername)
65 if bcache is None or not bcache.validfor(repo):
65 if bcache is None or not bcache.validfor(repo):
66 # cache object missing or cache object stale? Read from disk
66 # cache object missing or cache object stale? Read from disk
67 bcache = branchcache.fromfile(repo)
67 bcache = branchcache.fromfile(repo)
68
68
69 revs = []
69 revs = []
70 if bcache is None:
70 if bcache is None:
71 # no (fresh) cache available anymore, perhaps we can re-use
71 # no (fresh) cache available anymore, perhaps we can re-use
72 # the cache for a subset, then extend that to add info on missing
72 # the cache for a subset, then extend that to add info on missing
73 # revisions.
73 # revisions.
74 subsetname = subsettable.get(filtername)
74 subsetname = subsettable.get(filtername)
75 if subsetname is not None:
75 if subsetname is not None:
76 subset = repo.filtered(subsetname)
76 subset = repo.filtered(subsetname)
77 bcache = self[subset].copy()
77 bcache = self[subset].copy()
78 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
78 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
79 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
79 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
80 else:
80 else:
81 # nothing to fall back on, start empty.
81 # nothing to fall back on, start empty.
82 bcache = branchcache()
82 bcache = branchcache()
83
83
84 revs.extend(cl.revs(start=bcache.tiprev + 1))
84 revs.extend(cl.revs(start=bcache.tiprev + 1))
85 if revs:
85 if revs:
86 bcache.update(repo, revs)
86 bcache.update(repo, revs)
87
87
88 assert bcache.validfor(repo), filtername
88 assert bcache.validfor(repo), filtername
89 self._per_filter[repo.filtername] = bcache
89 self._per_filter[repo.filtername] = bcache
90
90
91 def replace(self, repo, remotebranchmap):
91 def replace(self, repo, remotebranchmap):
92 """Replace the branchmap cache for a repo with a branch mapping.
92 """Replace the branchmap cache for a repo with a branch mapping.
93
93
94 This is likely only called during clone with a branch map from a
94 This is likely only called during clone with a branch map from a
95 remote.
95 remote.
96
96
97 """
97 """
98 cl = repo.changelog
98 cl = repo.changelog
99 clrev = cl.rev
99 clrev = cl.rev
100 clbranchinfo = cl.branchinfo
100 clbranchinfo = cl.branchinfo
101 rbheads = []
101 rbheads = []
102 closed = []
102 closed = []
103 for bheads in remotebranchmap.itervalues():
103 for bheads in remotebranchmap.itervalues():
104 rbheads += bheads
104 rbheads += bheads
105 for h in bheads:
105 for h in bheads:
106 r = clrev(h)
106 r = clrev(h)
107 b, c = clbranchinfo(r)
107 b, c = clbranchinfo(r)
108 if c:
108 if c:
109 closed.append(h)
109 closed.append(h)
110
110
111 if rbheads:
111 if rbheads:
112 rtiprev = max((int(clrev(node)) for node in rbheads))
112 rtiprev = max((int(clrev(node)) for node in rbheads))
113 cache = branchcache(
113 cache = branchcache(
114 remotebranchmap, repo[rtiprev].node(), rtiprev,
114 remotebranchmap, repo[rtiprev].node(), rtiprev,
115 closednodes=closed)
115 closednodes=closed)
116
116
117 # Try to stick it as low as possible
117 # Try to stick it as low as possible
118 # filter above served are unlikely to be fetch from a clone
118 # filter above served are unlikely to be fetch from a clone
119 for candidate in ('base', 'immutable', 'served'):
119 for candidate in ('base', 'immutable', 'served'):
120 rview = repo.filtered(candidate)
120 rview = repo.filtered(candidate)
121 if cache.validfor(rview):
121 if cache.validfor(rview):
122 self._per_filter[candidate] = cache
122 self._per_filter[candidate] = cache
123 cache.write(rview)
123 cache.write(rview)
124 return
124 return
125
125
126 def clear(self):
126 def clear(self):
127 self._per_filter.clear()
127 self._per_filter.clear()
128
128
129 def _unknownnode(node):
129 def _unknownnode(node):
130 """ raises ValueError when branchcache found a node which does not exists
130 """ raises ValueError when branchcache found a node which does not exists
131 """
131 """
132 raise ValueError(r'node %s does not exist' % pycompat.sysstr(hex(node)))
132 raise ValueError(r'node %s does not exist' % pycompat.sysstr(hex(node)))
133
133
134 class branchcache(object):
134 class branchcache(object):
135 """A dict like object that hold branches heads cache.
135 """A dict like object that hold branches heads cache.
136
136
137 This cache is used to avoid costly computations to determine all the
137 This cache is used to avoid costly computations to determine all the
138 branch heads of a repo.
138 branch heads of a repo.
139
139
140 The cache is serialized on disk in the following format:
140 The cache is serialized on disk in the following format:
141
141
142 <tip hex node> <tip rev number> [optional filtered repo hex hash]
142 <tip hex node> <tip rev number> [optional filtered repo hex hash]
143 <branch head hex node> <open/closed state> <branch name>
143 <branch head hex node> <open/closed state> <branch name>
144 <branch head hex node> <open/closed state> <branch name>
144 <branch head hex node> <open/closed state> <branch name>
145 ...
145 ...
146
146
147 The first line is used to check if the cache is still valid. If the
147 The first line is used to check if the cache is still valid. If the
148 branch cache is for a filtered repo view, an optional third hash is
148 branch cache is for a filtered repo view, an optional third hash is
149 included that hashes the hashes of all filtered revisions.
149 included that hashes the hashes of all filtered revisions.
150
150
151 The open/closed state is represented by a single letter 'o' or 'c'.
151 The open/closed state is represented by a single letter 'o' or 'c'.
152 This field can be used to avoid changelog reads when determining if a
152 This field can be used to avoid changelog reads when determining if a
153 branch head closes a branch or not.
153 branch head closes a branch or not.
154 """
154 """
155
155
156 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
156 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
157 filteredhash=None, closednodes=None, hasnode=None):
157 filteredhash=None, closednodes=None, hasnode=None):
158 """ hasnode is a function which can be used to verify whether changelog
158 """ hasnode is a function which can be used to verify whether changelog
159 has a given node or not. If it's not provided, we assume that every node
159 has a given node or not. If it's not provided, we assume that every node
160 we have exists in changelog """
160 we have exists in changelog """
161 self.tipnode = tipnode
161 self.tipnode = tipnode
162 self.tiprev = tiprev
162 self.tiprev = tiprev
163 self.filteredhash = filteredhash
163 self.filteredhash = filteredhash
164 # closednodes is a set of nodes that close their branch. If the branch
164 # closednodes is a set of nodes that close their branch. If the branch
165 # cache has been updated, it may contain nodes that are no longer
165 # cache has been updated, it may contain nodes that are no longer
166 # heads.
166 # heads.
167 if closednodes is None:
167 if closednodes is None:
168 self._closednodes = set()
168 self._closednodes = set()
169 else:
169 else:
170 self._closednodes = closednodes
170 self._closednodes = closednodes
171 self._entries = dict(entries)
171 self._entries = dict(entries)
172 # whether closed nodes are verified or not
172 # whether closed nodes are verified or not
173 self._closedverified = False
173 self._closedverified = False
174 # branches for which nodes are verified
174 # branches for which nodes are verified
175 self._verifiedbranches = set()
175 self._verifiedbranches = set()
176 self._hasnode = hasnode
176 self._hasnode = hasnode
177 if self._hasnode is None:
177 if self._hasnode is None:
178 self._hasnode = lambda x: True
178 self._hasnode = lambda x: True
179
179
180 def _verifyclosed(self):
180 def _verifyclosed(self):
181 """ verify the closed nodes we have """
181 """ verify the closed nodes we have """
182 if self._closedverified:
182 if self._closedverified:
183 return
183 return
184 for node in self._closednodes:
184 for node in self._closednodes:
185 if not self._hasnode(node):
185 if not self._hasnode(node):
186 _unknownnode(node)
186 _unknownnode(node)
187
187
188 self._closedverified = True
188 self._closedverified = True
189
189
190 def _verifybranch(self, branch):
190 def _verifybranch(self, branch):
191 """ verify head nodes for the given branch. If branch is None, verify
191 """ verify head nodes for the given branch. If branch is None, verify
192 for all the branches """
192 for all the branches """
193 if branch not in self._entries or branch in self._verifiedbranches:
193 if branch not in self._entries or branch in self._verifiedbranches:
194 return
194 return
195 for n in self._entries[branch]:
195 for n in self._entries[branch]:
196 if not self._hasnode(n):
196 if not self._hasnode(n):
197 _unknownnode(n)
197 _unknownnode(n)
198
198
199 self._verifiedbranches.add(branch)
199 self._verifiedbranches.add(branch)
200
200
201 def _verifyall(self):
201 def _verifyall(self):
202 """ verifies nodes of all the branches """
202 """ verifies nodes of all the branches """
203 for b in self._entries:
203 for b in self._entries:
204 self._verifybranch(b)
204 self._verifybranch(b)
205
205
206 def __iter__(self):
206 def __iter__(self):
207 return iter(self._entries)
207 return iter(self._entries)
208
208
209 def __setitem__(self, key, value):
209 def __setitem__(self, key, value):
210 self._entries[key] = value
210 self._entries[key] = value
211
211
212 def __getitem__(self, key):
212 def __getitem__(self, key):
213 self._verifybranch(key)
213 self._verifybranch(key)
214 return self._entries[key]
214 return self._entries[key]
215
215
216 def __contains__(self, key):
216 def __contains__(self, key):
217 self._verifybranch(key)
217 self._verifybranch(key)
218 return key in self._entries
218 return key in self._entries
219
219
220 def iteritems(self):
220 def iteritems(self):
221 self._verifyall()
221 self._verifyall()
222 return self._entries.iteritems()
222 return self._entries.iteritems()
223
223
224 def hasbranch(self, label):
224 def hasbranch(self, label):
225 """ checks whether a branch of this name exists or not """
225 """ checks whether a branch of this name exists or not """
226 self._verifybranch(label)
226 self._verifybranch(label)
227 return label in self._entries
227 return label in self._entries
228
228
229 @classmethod
229 @classmethod
230 def fromfile(cls, repo):
230 def fromfile(cls, repo):
231 f = None
231 f = None
232 try:
232 try:
233 f = repo.cachevfs(cls._filename(repo))
233 f = repo.cachevfs(cls._filename(repo))
234 lineiter = iter(f)
234 lineiter = iter(f)
235 cachekey = next(lineiter).rstrip('\n').split(" ", 2)
235 cachekey = next(lineiter).rstrip('\n').split(" ", 2)
236 last, lrev = cachekey[:2]
236 last, lrev = cachekey[:2]
237 last, lrev = bin(last), int(lrev)
237 last, lrev = bin(last), int(lrev)
238 filteredhash = None
238 filteredhash = None
239 hasnode = repo.changelog.hasnode
239 hasnode = repo.changelog.hasnode
240 if len(cachekey) > 2:
240 if len(cachekey) > 2:
241 filteredhash = bin(cachekey[2])
241 filteredhash = bin(cachekey[2])
242 bcache = cls(tipnode=last, tiprev=lrev, filteredhash=filteredhash,
242 bcache = cls(tipnode=last, tiprev=lrev, filteredhash=filteredhash,
243 hasnode=hasnode)
243 hasnode=hasnode)
244 if not bcache.validfor(repo):
244 if not bcache.validfor(repo):
245 # invalidate the cache
245 # invalidate the cache
246 raise ValueError(r'tip differs')
246 raise ValueError(r'tip differs')
247 bcache.load(repo, lineiter)
247 bcache.load(repo, lineiter)
248 except (IOError, OSError):
248 except (IOError, OSError):
249 return None
249 return None
250
250
251 except Exception as inst:
251 except Exception as inst:
252 if repo.ui.debugflag:
252 if repo.ui.debugflag:
253 msg = 'invalid branchheads cache'
253 msg = 'invalid branchheads cache'
254 if repo.filtername is not None:
254 if repo.filtername is not None:
255 msg += ' (%s)' % repo.filtername
255 msg += ' (%s)' % repo.filtername
256 msg += ': %s\n'
256 msg += ': %s\n'
257 repo.ui.debug(msg % pycompat.bytestr(inst))
257 repo.ui.debug(msg % pycompat.bytestr(inst))
258 bcache = None
258 bcache = None
259
259
260 finally:
260 finally:
261 if f:
261 if f:
262 f.close()
262 f.close()
263
263
264 return bcache
264 return bcache
265
265
266 def load(self, repo, lineiter):
266 def load(self, repo, lineiter):
267 """ fully loads the branchcache by reading from the file using the line
267 """ fully loads the branchcache by reading from the file using the line
268 iterator passed"""
268 iterator passed"""
269 for line in lineiter:
269 for line in lineiter:
270 line = line.rstrip('\n')
270 line = line.rstrip('\n')
271 if not line:
271 if not line:
272 continue
272 continue
273 node, state, label = line.split(" ", 2)
273 node, state, label = line.split(" ", 2)
274 if state not in 'oc':
274 if state not in 'oc':
275 raise ValueError(r'invalid branch state')
275 raise ValueError(r'invalid branch state')
276 label = encoding.tolocal(label.strip())
276 label = encoding.tolocal(label.strip())
277 node = bin(node)
277 node = bin(node)
278 self._entries.setdefault(label, []).append(node)
278 self._entries.setdefault(label, []).append(node)
279 if state == 'c':
279 if state == 'c':
280 self._closednodes.add(node)
280 self._closednodes.add(node)
281
281
282 @staticmethod
282 @staticmethod
283 def _filename(repo):
283 def _filename(repo):
284 """name of a branchcache file for a given repo or repoview"""
284 """name of a branchcache file for a given repo or repoview"""
285 filename = "branch2"
285 filename = "branch2"
286 if repo.filtername:
286 if repo.filtername:
287 filename = '%s-%s' % (filename, repo.filtername)
287 filename = '%s-%s' % (filename, repo.filtername)
288 return filename
288 return filename
289
289
290 def validfor(self, repo):
290 def validfor(self, repo):
291 """Is the cache content valid regarding a repo
291 """Is the cache content valid regarding a repo
292
292
293 - False when cached tipnode is unknown or if we detect a strip.
293 - False when cached tipnode is unknown or if we detect a strip.
294 - True when cache is up to date or a subset of current repo."""
294 - True when cache is up to date or a subset of current repo."""
295 try:
295 try:
296 return ((self.tipnode == repo.changelog.node(self.tiprev))
296 return ((self.tipnode == repo.changelog.node(self.tiprev))
297 and (self.filteredhash ==
297 and (self.filteredhash ==
298 scmutil.filteredhash(repo, self.tiprev)))
298 scmutil.filteredhash(repo, self.tiprev)))
299 except IndexError:
299 except IndexError:
300 return False
300 return False
301
301
302 def _branchtip(self, heads):
302 def _branchtip(self, heads):
303 '''Return tuple with last open head in heads and false,
303 '''Return tuple with last open head in heads and false,
304 otherwise return last closed head and true.'''
304 otherwise return last closed head and true.'''
305 tip = heads[-1]
305 tip = heads[-1]
306 closed = True
306 closed = True
307 self._verifyclosed()
308 for h in reversed(heads):
307 for h in reversed(heads):
309 if h not in self._closednodes:
308 if h not in self._closednodes:
310 tip = h
309 tip = h
311 closed = False
310 closed = False
312 break
311 break
313 return tip, closed
312 return tip, closed
314
313
315 def branchtip(self, branch):
314 def branchtip(self, branch):
316 '''Return the tipmost open head on branch head, otherwise return the
315 '''Return the tipmost open head on branch head, otherwise return the
317 tipmost closed head on branch.
316 tipmost closed head on branch.
318 Raise KeyError for unknown branch.'''
317 Raise KeyError for unknown branch.'''
319 return self._branchtip(self[branch])[0]
318 return self._branchtip(self[branch])[0]
320
319
321 def iteropen(self, nodes):
320 def iteropen(self, nodes):
322 return (n for n in nodes if n not in self._closednodes)
321 return (n for n in nodes if n not in self._closednodes)
323
322
324 def branchheads(self, branch, closed=False):
323 def branchheads(self, branch, closed=False):
325 self._verifybranch(branch)
324 self._verifybranch(branch)
326 heads = self._entries[branch]
325 heads = self._entries[branch]
327 if not closed:
326 if not closed:
328 heads = list(self.iteropen(heads))
327 heads = list(self.iteropen(heads))
329 return heads
328 return heads
330
329
331 def iterbranches(self):
330 def iterbranches(self):
332 for bn, heads in self.iteritems():
331 for bn, heads in self.iteritems():
333 yield (bn, heads) + self._branchtip(heads)
332 yield (bn, heads) + self._branchtip(heads)
334
333
335 def iterheads(self):
334 def iterheads(self):
336 """ returns all the heads """
335 """ returns all the heads """
337 self._verifyall()
336 self._verifyall()
338 return self._entries.itervalues()
337 return self._entries.itervalues()
339
338
340 def copy(self):
339 def copy(self):
341 """return an deep copy of the branchcache object"""
340 """return an deep copy of the branchcache object"""
342 self._verifyall()
341 self._verifyall()
343 return type(self)(
342 return type(self)(
344 self._entries, self.tipnode, self.tiprev, self.filteredhash,
343 self._entries, self.tipnode, self.tiprev, self.filteredhash,
345 self._closednodes)
344 self._closednodes)
346
345
347 def write(self, repo):
346 def write(self, repo):
348 try:
347 try:
349 f = repo.cachevfs(self._filename(repo), "w", atomictemp=True)
348 f = repo.cachevfs(self._filename(repo), "w", atomictemp=True)
350 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
349 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
351 if self.filteredhash is not None:
350 if self.filteredhash is not None:
352 cachekey.append(hex(self.filteredhash))
351 cachekey.append(hex(self.filteredhash))
353 f.write(" ".join(cachekey) + '\n')
352 f.write(" ".join(cachekey) + '\n')
354 nodecount = 0
353 nodecount = 0
355 for label, nodes in sorted(self.iteritems()):
354 for label, nodes in sorted(self.iteritems()):
356 label = encoding.fromlocal(label)
355 label = encoding.fromlocal(label)
357 for node in nodes:
356 for node in nodes:
358 nodecount += 1
357 nodecount += 1
359 if node in self._closednodes:
358 if node in self._closednodes:
360 state = 'c'
359 state = 'c'
361 else:
360 else:
362 state = 'o'
361 state = 'o'
363 f.write("%s %s %s\n" % (hex(node), state, label))
362 f.write("%s %s %s\n" % (hex(node), state, label))
364 f.close()
363 f.close()
365 repo.ui.log('branchcache',
364 repo.ui.log('branchcache',
366 'wrote %s branch cache with %d labels and %d nodes\n',
365 'wrote %s branch cache with %d labels and %d nodes\n',
367 repo.filtername, len(self._entries), nodecount)
366 repo.filtername, len(self._entries), nodecount)
368 except (IOError, OSError, error.Abort) as inst:
367 except (IOError, OSError, error.Abort) as inst:
369 # Abort may be raised by read only opener, so log and continue
368 # Abort may be raised by read only opener, so log and continue
370 repo.ui.debug("couldn't write branch cache: %s\n" %
369 repo.ui.debug("couldn't write branch cache: %s\n" %
371 stringutil.forcebytestr(inst))
370 stringutil.forcebytestr(inst))
372
371
373 def update(self, repo, revgen):
372 def update(self, repo, revgen):
374 """Given a branchhead cache, self, that may have extra nodes or be
373 """Given a branchhead cache, self, that may have extra nodes or be
375 missing heads, and a generator of nodes that are strictly a superset of
374 missing heads, and a generator of nodes that are strictly a superset of
376 heads missing, this function updates self to be correct.
375 heads missing, this function updates self to be correct.
377 """
376 """
378 starttime = util.timer()
377 starttime = util.timer()
379 cl = repo.changelog
378 cl = repo.changelog
380 # collect new branch entries
379 # collect new branch entries
381 newbranches = {}
380 newbranches = {}
382 getbranchinfo = repo.revbranchcache().branchinfo
381 getbranchinfo = repo.revbranchcache().branchinfo
383 for r in revgen:
382 for r in revgen:
384 branch, closesbranch = getbranchinfo(r)
383 branch, closesbranch = getbranchinfo(r)
385 newbranches.setdefault(branch, []).append(r)
384 newbranches.setdefault(branch, []).append(r)
386 if closesbranch:
385 if closesbranch:
387 self._closednodes.add(cl.node(r))
386 self._closednodes.add(cl.node(r))
388
387
389 # fetch current topological heads to speed up filtering
388 # fetch current topological heads to speed up filtering
390 topoheads = set(cl.headrevs())
389 topoheads = set(cl.headrevs())
391
390
392 # if older branchheads are reachable from new ones, they aren't
391 # if older branchheads are reachable from new ones, they aren't
393 # really branchheads. Note checking parents is insufficient:
392 # really branchheads. Note checking parents is insufficient:
394 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
393 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
395 for branch, newheadrevs in newbranches.iteritems():
394 for branch, newheadrevs in newbranches.iteritems():
396 bheads = self._entries.setdefault(branch, [])
395 bheads = self._entries.setdefault(branch, [])
397 bheadset = set(cl.rev(node) for node in bheads)
396 bheadset = set(cl.rev(node) for node in bheads)
398
397
399 # This have been tested True on all internal usage of this function.
398 # This have been tested True on all internal usage of this function.
400 # run it again in case of doubt
399 # run it again in case of doubt
401 # assert not (set(bheadrevs) & set(newheadrevs))
400 # assert not (set(bheadrevs) & set(newheadrevs))
402 bheadset.update(newheadrevs)
401 bheadset.update(newheadrevs)
403
402
404 # This prunes out two kinds of heads - heads that are superseded by
403 # This prunes out two kinds of heads - heads that are superseded by
405 # a head in newheadrevs, and newheadrevs that are not heads because
404 # a head in newheadrevs, and newheadrevs that are not heads because
406 # an existing head is their descendant.
405 # an existing head is their descendant.
407 uncertain = bheadset - topoheads
406 uncertain = bheadset - topoheads
408 if uncertain:
407 if uncertain:
409 floorrev = min(uncertain)
408 floorrev = min(uncertain)
410 ancestors = set(cl.ancestors(newheadrevs, floorrev))
409 ancestors = set(cl.ancestors(newheadrevs, floorrev))
411 bheadset -= ancestors
410 bheadset -= ancestors
412 bheadrevs = sorted(bheadset)
411 bheadrevs = sorted(bheadset)
413 self[branch] = [cl.node(rev) for rev in bheadrevs]
412 self[branch] = [cl.node(rev) for rev in bheadrevs]
414 tiprev = bheadrevs[-1]
413 tiprev = bheadrevs[-1]
415 if tiprev > self.tiprev:
414 if tiprev > self.tiprev:
416 self.tipnode = cl.node(tiprev)
415 self.tipnode = cl.node(tiprev)
417 self.tiprev = tiprev
416 self.tiprev = tiprev
418
417
419 if not self.validfor(repo):
418 if not self.validfor(repo):
420 # cache key are not valid anymore
419 # cache key are not valid anymore
421 self.tipnode = nullid
420 self.tipnode = nullid
422 self.tiprev = nullrev
421 self.tiprev = nullrev
423 for heads in self.iterheads():
422 for heads in self.iterheads():
424 tiprev = max(cl.rev(node) for node in heads)
423 tiprev = max(cl.rev(node) for node in heads)
425 if tiprev > self.tiprev:
424 if tiprev > self.tiprev:
426 self.tipnode = cl.node(tiprev)
425 self.tipnode = cl.node(tiprev)
427 self.tiprev = tiprev
426 self.tiprev = tiprev
428 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
427 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
429
428
430 duration = util.timer() - starttime
429 duration = util.timer() - starttime
431 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
430 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
432 repo.filtername or b'None', duration)
431 repo.filtername or b'None', duration)
433
432
434 self.write(repo)
433 self.write(repo)
435
434
436
435
437 class remotebranchcache(branchcache):
436 class remotebranchcache(branchcache):
438 """Branchmap info for a remote connection, should not write locally"""
437 """Branchmap info for a remote connection, should not write locally"""
439 def write(self, repo):
438 def write(self, repo):
440 pass
439 pass
441
440
442
441
443 # Revision branch info cache
442 # Revision branch info cache
444
443
445 _rbcversion = '-v1'
444 _rbcversion = '-v1'
446 _rbcnames = 'rbc-names' + _rbcversion
445 _rbcnames = 'rbc-names' + _rbcversion
447 _rbcrevs = 'rbc-revs' + _rbcversion
446 _rbcrevs = 'rbc-revs' + _rbcversion
448 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
447 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
449 _rbcrecfmt = '>4sI'
448 _rbcrecfmt = '>4sI'
450 _rbcrecsize = calcsize(_rbcrecfmt)
449 _rbcrecsize = calcsize(_rbcrecfmt)
451 _rbcnodelen = 4
450 _rbcnodelen = 4
452 _rbcbranchidxmask = 0x7fffffff
451 _rbcbranchidxmask = 0x7fffffff
453 _rbccloseflag = 0x80000000
452 _rbccloseflag = 0x80000000
454
453
455 class revbranchcache(object):
454 class revbranchcache(object):
456 """Persistent cache, mapping from revision number to branch name and close.
455 """Persistent cache, mapping from revision number to branch name and close.
457 This is a low level cache, independent of filtering.
456 This is a low level cache, independent of filtering.
458
457
459 Branch names are stored in rbc-names in internal encoding separated by 0.
458 Branch names are stored in rbc-names in internal encoding separated by 0.
460 rbc-names is append-only, and each branch name is only stored once and will
459 rbc-names is append-only, and each branch name is only stored once and will
461 thus have a unique index.
460 thus have a unique index.
462
461
463 The branch info for each revision is stored in rbc-revs as constant size
462 The branch info for each revision is stored in rbc-revs as constant size
464 records. The whole file is read into memory, but it is only 'parsed' on
463 records. The whole file is read into memory, but it is only 'parsed' on
465 demand. The file is usually append-only but will be truncated if repo
464 demand. The file is usually append-only but will be truncated if repo
466 modification is detected.
465 modification is detected.
467 The record for each revision contains the first 4 bytes of the
466 The record for each revision contains the first 4 bytes of the
468 corresponding node hash, and the record is only used if it still matches.
467 corresponding node hash, and the record is only used if it still matches.
469 Even a completely trashed rbc-revs fill thus still give the right result
468 Even a completely trashed rbc-revs fill thus still give the right result
470 while converging towards full recovery ... assuming no incorrectly matching
469 while converging towards full recovery ... assuming no incorrectly matching
471 node hashes.
470 node hashes.
472 The record also contains 4 bytes where 31 bits contains the index of the
471 The record also contains 4 bytes where 31 bits contains the index of the
473 branch and the last bit indicate that it is a branch close commit.
472 branch and the last bit indicate that it is a branch close commit.
474 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
473 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
475 and will grow with it but be 1/8th of its size.
474 and will grow with it but be 1/8th of its size.
476 """
475 """
477
476
478 def __init__(self, repo, readonly=True):
477 def __init__(self, repo, readonly=True):
479 assert repo.filtername is None
478 assert repo.filtername is None
480 self._repo = repo
479 self._repo = repo
481 self._names = [] # branch names in local encoding with static index
480 self._names = [] # branch names in local encoding with static index
482 self._rbcrevs = bytearray()
481 self._rbcrevs = bytearray()
483 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
482 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
484 try:
483 try:
485 bndata = repo.cachevfs.read(_rbcnames)
484 bndata = repo.cachevfs.read(_rbcnames)
486 self._rbcsnameslen = len(bndata) # for verification before writing
485 self._rbcsnameslen = len(bndata) # for verification before writing
487 if bndata:
486 if bndata:
488 self._names = [encoding.tolocal(bn)
487 self._names = [encoding.tolocal(bn)
489 for bn in bndata.split('\0')]
488 for bn in bndata.split('\0')]
490 except (IOError, OSError):
489 except (IOError, OSError):
491 if readonly:
490 if readonly:
492 # don't try to use cache - fall back to the slow path
491 # don't try to use cache - fall back to the slow path
493 self.branchinfo = self._branchinfo
492 self.branchinfo = self._branchinfo
494
493
495 if self._names:
494 if self._names:
496 try:
495 try:
497 data = repo.cachevfs.read(_rbcrevs)
496 data = repo.cachevfs.read(_rbcrevs)
498 self._rbcrevs[:] = data
497 self._rbcrevs[:] = data
499 except (IOError, OSError) as inst:
498 except (IOError, OSError) as inst:
500 repo.ui.debug("couldn't read revision branch cache: %s\n" %
499 repo.ui.debug("couldn't read revision branch cache: %s\n" %
501 stringutil.forcebytestr(inst))
500 stringutil.forcebytestr(inst))
502 # remember number of good records on disk
501 # remember number of good records on disk
503 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
502 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
504 len(repo.changelog))
503 len(repo.changelog))
505 if self._rbcrevslen == 0:
504 if self._rbcrevslen == 0:
506 self._names = []
505 self._names = []
507 self._rbcnamescount = len(self._names) # number of names read at
506 self._rbcnamescount = len(self._names) # number of names read at
508 # _rbcsnameslen
507 # _rbcsnameslen
509
508
510 def _clear(self):
509 def _clear(self):
511 self._rbcsnameslen = 0
510 self._rbcsnameslen = 0
512 del self._names[:]
511 del self._names[:]
513 self._rbcnamescount = 0
512 self._rbcnamescount = 0
514 self._rbcrevslen = len(self._repo.changelog)
513 self._rbcrevslen = len(self._repo.changelog)
515 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
514 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
516 util.clearcachedproperty(self, '_namesreverse')
515 util.clearcachedproperty(self, '_namesreverse')
517
516
518 @util.propertycache
517 @util.propertycache
519 def _namesreverse(self):
518 def _namesreverse(self):
520 return dict((b, r) for r, b in enumerate(self._names))
519 return dict((b, r) for r, b in enumerate(self._names))
521
520
522 def branchinfo(self, rev):
521 def branchinfo(self, rev):
523 """Return branch name and close flag for rev, using and updating
522 """Return branch name and close flag for rev, using and updating
524 persistent cache."""
523 persistent cache."""
525 changelog = self._repo.changelog
524 changelog = self._repo.changelog
526 rbcrevidx = rev * _rbcrecsize
525 rbcrevidx = rev * _rbcrecsize
527
526
528 # avoid negative index, changelog.read(nullrev) is fast without cache
527 # avoid negative index, changelog.read(nullrev) is fast without cache
529 if rev == nullrev:
528 if rev == nullrev:
530 return changelog.branchinfo(rev)
529 return changelog.branchinfo(rev)
531
530
532 # if requested rev isn't allocated, grow and cache the rev info
531 # if requested rev isn't allocated, grow and cache the rev info
533 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
532 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
534 return self._branchinfo(rev)
533 return self._branchinfo(rev)
535
534
536 # fast path: extract data from cache, use it if node is matching
535 # fast path: extract data from cache, use it if node is matching
537 reponode = changelog.node(rev)[:_rbcnodelen]
536 reponode = changelog.node(rev)[:_rbcnodelen]
538 cachenode, branchidx = unpack_from(
537 cachenode, branchidx = unpack_from(
539 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
538 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
540 close = bool(branchidx & _rbccloseflag)
539 close = bool(branchidx & _rbccloseflag)
541 if close:
540 if close:
542 branchidx &= _rbcbranchidxmask
541 branchidx &= _rbcbranchidxmask
543 if cachenode == '\0\0\0\0':
542 if cachenode == '\0\0\0\0':
544 pass
543 pass
545 elif cachenode == reponode:
544 elif cachenode == reponode:
546 try:
545 try:
547 return self._names[branchidx], close
546 return self._names[branchidx], close
548 except IndexError:
547 except IndexError:
549 # recover from invalid reference to unknown branch
548 # recover from invalid reference to unknown branch
550 self._repo.ui.debug("referenced branch names not found"
549 self._repo.ui.debug("referenced branch names not found"
551 " - rebuilding revision branch cache from scratch\n")
550 " - rebuilding revision branch cache from scratch\n")
552 self._clear()
551 self._clear()
553 else:
552 else:
554 # rev/node map has changed, invalidate the cache from here up
553 # rev/node map has changed, invalidate the cache from here up
555 self._repo.ui.debug("history modification detected - truncating "
554 self._repo.ui.debug("history modification detected - truncating "
556 "revision branch cache to revision %d\n" % rev)
555 "revision branch cache to revision %d\n" % rev)
557 truncate = rbcrevidx + _rbcrecsize
556 truncate = rbcrevidx + _rbcrecsize
558 del self._rbcrevs[truncate:]
557 del self._rbcrevs[truncate:]
559 self._rbcrevslen = min(self._rbcrevslen, truncate)
558 self._rbcrevslen = min(self._rbcrevslen, truncate)
560
559
561 # fall back to slow path and make sure it will be written to disk
560 # fall back to slow path and make sure it will be written to disk
562 return self._branchinfo(rev)
561 return self._branchinfo(rev)
563
562
564 def _branchinfo(self, rev):
563 def _branchinfo(self, rev):
565 """Retrieve branch info from changelog and update _rbcrevs"""
564 """Retrieve branch info from changelog and update _rbcrevs"""
566 changelog = self._repo.changelog
565 changelog = self._repo.changelog
567 b, close = changelog.branchinfo(rev)
566 b, close = changelog.branchinfo(rev)
568 if b in self._namesreverse:
567 if b in self._namesreverse:
569 branchidx = self._namesreverse[b]
568 branchidx = self._namesreverse[b]
570 else:
569 else:
571 branchidx = len(self._names)
570 branchidx = len(self._names)
572 self._names.append(b)
571 self._names.append(b)
573 self._namesreverse[b] = branchidx
572 self._namesreverse[b] = branchidx
574 reponode = changelog.node(rev)
573 reponode = changelog.node(rev)
575 if close:
574 if close:
576 branchidx |= _rbccloseflag
575 branchidx |= _rbccloseflag
577 self._setcachedata(rev, reponode, branchidx)
576 self._setcachedata(rev, reponode, branchidx)
578 return b, close
577 return b, close
579
578
580 def setdata(self, branch, rev, node, close):
579 def setdata(self, branch, rev, node, close):
581 """add new data information to the cache"""
580 """add new data information to the cache"""
582 if branch in self._namesreverse:
581 if branch in self._namesreverse:
583 branchidx = self._namesreverse[branch]
582 branchidx = self._namesreverse[branch]
584 else:
583 else:
585 branchidx = len(self._names)
584 branchidx = len(self._names)
586 self._names.append(branch)
585 self._names.append(branch)
587 self._namesreverse[branch] = branchidx
586 self._namesreverse[branch] = branchidx
588 if close:
587 if close:
589 branchidx |= _rbccloseflag
588 branchidx |= _rbccloseflag
590 self._setcachedata(rev, node, branchidx)
589 self._setcachedata(rev, node, branchidx)
591 # If no cache data were readable (non exists, bad permission, etc)
590 # If no cache data were readable (non exists, bad permission, etc)
592 # the cache was bypassing itself by setting:
591 # the cache was bypassing itself by setting:
593 #
592 #
594 # self.branchinfo = self._branchinfo
593 # self.branchinfo = self._branchinfo
595 #
594 #
596 # Since we now have data in the cache, we need to drop this bypassing.
595 # Since we now have data in the cache, we need to drop this bypassing.
597 if r'branchinfo' in vars(self):
596 if r'branchinfo' in vars(self):
598 del self.branchinfo
597 del self.branchinfo
599
598
600 def _setcachedata(self, rev, node, branchidx):
599 def _setcachedata(self, rev, node, branchidx):
601 """Writes the node's branch data to the in-memory cache data."""
600 """Writes the node's branch data to the in-memory cache data."""
602 if rev == nullrev:
601 if rev == nullrev:
603 return
602 return
604 rbcrevidx = rev * _rbcrecsize
603 rbcrevidx = rev * _rbcrecsize
605 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
604 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
606 self._rbcrevs.extend('\0' *
605 self._rbcrevs.extend('\0' *
607 (len(self._repo.changelog) * _rbcrecsize -
606 (len(self._repo.changelog) * _rbcrecsize -
608 len(self._rbcrevs)))
607 len(self._rbcrevs)))
609 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
608 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
610 self._rbcrevslen = min(self._rbcrevslen, rev)
609 self._rbcrevslen = min(self._rbcrevslen, rev)
611
610
612 tr = self._repo.currenttransaction()
611 tr = self._repo.currenttransaction()
613 if tr:
612 if tr:
614 tr.addfinalize('write-revbranchcache', self.write)
613 tr.addfinalize('write-revbranchcache', self.write)
615
614
616 def write(self, tr=None):
615 def write(self, tr=None):
617 """Save branch cache if it is dirty."""
616 """Save branch cache if it is dirty."""
618 repo = self._repo
617 repo = self._repo
619 wlock = None
618 wlock = None
620 step = ''
619 step = ''
621 try:
620 try:
622 if self._rbcnamescount < len(self._names):
621 if self._rbcnamescount < len(self._names):
623 step = ' names'
622 step = ' names'
624 wlock = repo.wlock(wait=False)
623 wlock = repo.wlock(wait=False)
625 if self._rbcnamescount != 0:
624 if self._rbcnamescount != 0:
626 f = repo.cachevfs.open(_rbcnames, 'ab')
625 f = repo.cachevfs.open(_rbcnames, 'ab')
627 if f.tell() == self._rbcsnameslen:
626 if f.tell() == self._rbcsnameslen:
628 f.write('\0')
627 f.write('\0')
629 else:
628 else:
630 f.close()
629 f.close()
631 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
630 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
632 self._rbcnamescount = 0
631 self._rbcnamescount = 0
633 self._rbcrevslen = 0
632 self._rbcrevslen = 0
634 if self._rbcnamescount == 0:
633 if self._rbcnamescount == 0:
635 # before rewriting names, make sure references are removed
634 # before rewriting names, make sure references are removed
636 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
635 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
637 f = repo.cachevfs.open(_rbcnames, 'wb')
636 f = repo.cachevfs.open(_rbcnames, 'wb')
638 f.write('\0'.join(encoding.fromlocal(b)
637 f.write('\0'.join(encoding.fromlocal(b)
639 for b in self._names[self._rbcnamescount:]))
638 for b in self._names[self._rbcnamescount:]))
640 self._rbcsnameslen = f.tell()
639 self._rbcsnameslen = f.tell()
641 f.close()
640 f.close()
642 self._rbcnamescount = len(self._names)
641 self._rbcnamescount = len(self._names)
643
642
644 start = self._rbcrevslen * _rbcrecsize
643 start = self._rbcrevslen * _rbcrecsize
645 if start != len(self._rbcrevs):
644 if start != len(self._rbcrevs):
646 step = ''
645 step = ''
647 if wlock is None:
646 if wlock is None:
648 wlock = repo.wlock(wait=False)
647 wlock = repo.wlock(wait=False)
649 revs = min(len(repo.changelog),
648 revs = min(len(repo.changelog),
650 len(self._rbcrevs) // _rbcrecsize)
649 len(self._rbcrevs) // _rbcrecsize)
651 f = repo.cachevfs.open(_rbcrevs, 'ab')
650 f = repo.cachevfs.open(_rbcrevs, 'ab')
652 if f.tell() != start:
651 if f.tell() != start:
653 repo.ui.debug("truncating cache/%s to %d\n"
652 repo.ui.debug("truncating cache/%s to %d\n"
654 % (_rbcrevs, start))
653 % (_rbcrevs, start))
655 f.seek(start)
654 f.seek(start)
656 if f.tell() != start:
655 if f.tell() != start:
657 start = 0
656 start = 0
658 f.seek(start)
657 f.seek(start)
659 f.truncate()
658 f.truncate()
660 end = revs * _rbcrecsize
659 end = revs * _rbcrecsize
661 f.write(self._rbcrevs[start:end])
660 f.write(self._rbcrevs[start:end])
662 f.close()
661 f.close()
663 self._rbcrevslen = revs
662 self._rbcrevslen = revs
664 except (IOError, OSError, error.Abort, error.LockError) as inst:
663 except (IOError, OSError, error.Abort, error.LockError) as inst:
665 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
664 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
666 % (step, stringutil.forcebytestr(inst)))
665 % (step, stringutil.forcebytestr(inst)))
667 finally:
666 finally:
668 if wlock is not None:
667 if wlock is not None:
669 wlock.release()
668 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now