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