##// END OF EJS Templates
branchmap: don't use buffer() on Python 3...
Augie Fackler -
r31347:279430ee default
parent child Browse files
Show More
@@ -1,520 +1,520 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 array
10 import array
11 import struct
11 import struct
12
12
13 from .node import (
13 from .node import (
14 bin,
14 bin,
15 hex,
15 hex,
16 nullid,
16 nullid,
17 nullrev,
17 nullrev,
18 )
18 )
19 from . import (
19 from . import (
20 encoding,
20 encoding,
21 error,
21 error,
22 scmutil,
22 scmutil,
23 util,
23 util,
24 )
24 )
25
25
26 array = array.array
26 array = array.array
27 calcsize = struct.calcsize
27 calcsize = struct.calcsize
28 pack = struct.pack
28 pack = struct.pack
29 unpack = struct.unpack
29 unpack = struct.unpack
30
30
31 def _filename(repo):
31 def _filename(repo):
32 """name of a branchcache file for a given repo or repoview"""
32 """name of a branchcache file for a given repo or repoview"""
33 filename = "cache/branch2"
33 filename = "cache/branch2"
34 if repo.filtername:
34 if repo.filtername:
35 filename = '%s-%s' % (filename, repo.filtername)
35 filename = '%s-%s' % (filename, repo.filtername)
36 return filename
36 return filename
37
37
38 def read(repo):
38 def read(repo):
39 try:
39 try:
40 f = repo.vfs(_filename(repo))
40 f = repo.vfs(_filename(repo))
41 lines = f.read().split('\n')
41 lines = f.read().split('\n')
42 f.close()
42 f.close()
43 except (IOError, OSError):
43 except (IOError, OSError):
44 return None
44 return None
45
45
46 try:
46 try:
47 cachekey = lines.pop(0).split(" ", 2)
47 cachekey = lines.pop(0).split(" ", 2)
48 last, lrev = cachekey[:2]
48 last, lrev = cachekey[:2]
49 last, lrev = bin(last), int(lrev)
49 last, lrev = bin(last), int(lrev)
50 filteredhash = None
50 filteredhash = None
51 if len(cachekey) > 2:
51 if len(cachekey) > 2:
52 filteredhash = bin(cachekey[2])
52 filteredhash = bin(cachekey[2])
53 partial = branchcache(tipnode=last, tiprev=lrev,
53 partial = branchcache(tipnode=last, tiprev=lrev,
54 filteredhash=filteredhash)
54 filteredhash=filteredhash)
55 if not partial.validfor(repo):
55 if not partial.validfor(repo):
56 # invalidate the cache
56 # invalidate the cache
57 raise ValueError('tip differs')
57 raise ValueError('tip differs')
58 cl = repo.changelog
58 cl = repo.changelog
59 for l in lines:
59 for l in lines:
60 if not l:
60 if not l:
61 continue
61 continue
62 node, state, label = l.split(" ", 2)
62 node, state, label = l.split(" ", 2)
63 if state not in 'oc':
63 if state not in 'oc':
64 raise ValueError('invalid branch state')
64 raise ValueError('invalid branch state')
65 label = encoding.tolocal(label.strip())
65 label = encoding.tolocal(label.strip())
66 node = bin(node)
66 node = bin(node)
67 if not cl.hasnode(node):
67 if not cl.hasnode(node):
68 raise ValueError('node %s does not exist' % hex(node))
68 raise ValueError('node %s does not exist' % hex(node))
69 partial.setdefault(label, []).append(node)
69 partial.setdefault(label, []).append(node)
70 if state == 'c':
70 if state == 'c':
71 partial._closednodes.add(node)
71 partial._closednodes.add(node)
72 except KeyboardInterrupt:
72 except KeyboardInterrupt:
73 raise
73 raise
74 except Exception as inst:
74 except Exception as inst:
75 if repo.ui.debugflag:
75 if repo.ui.debugflag:
76 msg = 'invalid branchheads cache'
76 msg = 'invalid branchheads cache'
77 if repo.filtername is not None:
77 if repo.filtername is not None:
78 msg += ' (%s)' % repo.filtername
78 msg += ' (%s)' % repo.filtername
79 msg += ': %s\n'
79 msg += ': %s\n'
80 repo.ui.debug(msg % inst)
80 repo.ui.debug(msg % inst)
81 partial = None
81 partial = None
82 return partial
82 return partial
83
83
84 ### Nearest subset relation
84 ### Nearest subset relation
85 # Nearest subset of filter X is a filter Y so that:
85 # Nearest subset of filter X is a filter Y so that:
86 # * Y is included in X,
86 # * Y is included in X,
87 # * X - Y is as small as possible.
87 # * X - Y is as small as possible.
88 # This create and ordering used for branchmap purpose.
88 # This create and ordering used for branchmap purpose.
89 # the ordering may be partial
89 # the ordering may be partial
90 subsettable = {None: 'visible',
90 subsettable = {None: 'visible',
91 'visible': 'served',
91 'visible': 'served',
92 'served': 'immutable',
92 'served': 'immutable',
93 'immutable': 'base'}
93 'immutable': 'base'}
94
94
95 def updatecache(repo):
95 def updatecache(repo):
96 cl = repo.changelog
96 cl = repo.changelog
97 filtername = repo.filtername
97 filtername = repo.filtername
98 partial = repo._branchcaches.get(filtername)
98 partial = repo._branchcaches.get(filtername)
99
99
100 revs = []
100 revs = []
101 if partial is None or not partial.validfor(repo):
101 if partial is None or not partial.validfor(repo):
102 partial = read(repo)
102 partial = read(repo)
103 if partial is None:
103 if partial is None:
104 subsetname = subsettable.get(filtername)
104 subsetname = subsettable.get(filtername)
105 if subsetname is None:
105 if subsetname is None:
106 partial = branchcache()
106 partial = branchcache()
107 else:
107 else:
108 subset = repo.filtered(subsetname)
108 subset = repo.filtered(subsetname)
109 partial = subset.branchmap().copy()
109 partial = subset.branchmap().copy()
110 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
110 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
111 revs.extend(r for r in extrarevs if r <= partial.tiprev)
111 revs.extend(r for r in extrarevs if r <= partial.tiprev)
112 revs.extend(cl.revs(start=partial.tiprev + 1))
112 revs.extend(cl.revs(start=partial.tiprev + 1))
113 if revs:
113 if revs:
114 partial.update(repo, revs)
114 partial.update(repo, revs)
115 partial.write(repo)
115 partial.write(repo)
116
116
117 assert partial.validfor(repo), filtername
117 assert partial.validfor(repo), filtername
118 repo._branchcaches[repo.filtername] = partial
118 repo._branchcaches[repo.filtername] = partial
119
119
120 def replacecache(repo, bm):
120 def replacecache(repo, bm):
121 """Replace the branchmap cache for a repo with a branch mapping.
121 """Replace the branchmap cache for a repo with a branch mapping.
122
122
123 This is likely only called during clone with a branch map from a remote.
123 This is likely only called during clone with a branch map from a remote.
124 """
124 """
125 rbheads = []
125 rbheads = []
126 closed = []
126 closed = []
127 for bheads in bm.itervalues():
127 for bheads in bm.itervalues():
128 rbheads.extend(bheads)
128 rbheads.extend(bheads)
129 for h in bheads:
129 for h in bheads:
130 r = repo.changelog.rev(h)
130 r = repo.changelog.rev(h)
131 b, c = repo.changelog.branchinfo(r)
131 b, c = repo.changelog.branchinfo(r)
132 if c:
132 if c:
133 closed.append(h)
133 closed.append(h)
134
134
135 if rbheads:
135 if rbheads:
136 rtiprev = max((int(repo.changelog.rev(node))
136 rtiprev = max((int(repo.changelog.rev(node))
137 for node in rbheads))
137 for node in rbheads))
138 cache = branchcache(bm,
138 cache = branchcache(bm,
139 repo[rtiprev].node(),
139 repo[rtiprev].node(),
140 rtiprev,
140 rtiprev,
141 closednodes=closed)
141 closednodes=closed)
142
142
143 # Try to stick it as low as possible
143 # Try to stick it as low as possible
144 # filter above served are unlikely to be fetch from a clone
144 # filter above served are unlikely to be fetch from a clone
145 for candidate in ('base', 'immutable', 'served'):
145 for candidate in ('base', 'immutable', 'served'):
146 rview = repo.filtered(candidate)
146 rview = repo.filtered(candidate)
147 if cache.validfor(rview):
147 if cache.validfor(rview):
148 repo._branchcaches[candidate] = cache
148 repo._branchcaches[candidate] = cache
149 cache.write(rview)
149 cache.write(rview)
150 break
150 break
151
151
152 class branchcache(dict):
152 class branchcache(dict):
153 """A dict like object that hold branches heads cache.
153 """A dict like object that hold branches heads cache.
154
154
155 This cache is used to avoid costly computations to determine all the
155 This cache is used to avoid costly computations to determine all the
156 branch heads of a repo.
156 branch heads of a repo.
157
157
158 The cache is serialized on disk in the following format:
158 The cache is serialized on disk in the following format:
159
159
160 <tip hex node> <tip rev number> [optional filtered repo hex hash]
160 <tip hex node> <tip rev number> [optional filtered repo hex hash]
161 <branch head hex node> <open/closed state> <branch name>
161 <branch head hex node> <open/closed state> <branch name>
162 <branch head hex node> <open/closed state> <branch name>
162 <branch head hex node> <open/closed state> <branch name>
163 ...
163 ...
164
164
165 The first line is used to check if the cache is still valid. If the
165 The first line is used to check if the cache is still valid. If the
166 branch cache is for a filtered repo view, an optional third hash is
166 branch cache is for a filtered repo view, an optional third hash is
167 included that hashes the hashes of all filtered revisions.
167 included that hashes the hashes of all filtered revisions.
168
168
169 The open/closed state is represented by a single letter 'o' or 'c'.
169 The open/closed state is represented by a single letter 'o' or 'c'.
170 This field can be used to avoid changelog reads when determining if a
170 This field can be used to avoid changelog reads when determining if a
171 branch head closes a branch or not.
171 branch head closes a branch or not.
172 """
172 """
173
173
174 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
174 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
175 filteredhash=None, closednodes=None):
175 filteredhash=None, closednodes=None):
176 super(branchcache, self).__init__(entries)
176 super(branchcache, self).__init__(entries)
177 self.tipnode = tipnode
177 self.tipnode = tipnode
178 self.tiprev = tiprev
178 self.tiprev = tiprev
179 self.filteredhash = filteredhash
179 self.filteredhash = filteredhash
180 # closednodes is a set of nodes that close their branch. If the branch
180 # closednodes is a set of nodes that close their branch. If the branch
181 # cache has been updated, it may contain nodes that are no longer
181 # cache has been updated, it may contain nodes that are no longer
182 # heads.
182 # heads.
183 if closednodes is None:
183 if closednodes is None:
184 self._closednodes = set()
184 self._closednodes = set()
185 else:
185 else:
186 self._closednodes = closednodes
186 self._closednodes = closednodes
187
187
188 def validfor(self, repo):
188 def validfor(self, repo):
189 """Is the cache content valid regarding a repo
189 """Is the cache content valid regarding a repo
190
190
191 - False when cached tipnode is unknown or if we detect a strip.
191 - False when cached tipnode is unknown or if we detect a strip.
192 - True when cache is up to date or a subset of current repo."""
192 - True when cache is up to date or a subset of current repo."""
193 try:
193 try:
194 return ((self.tipnode == repo.changelog.node(self.tiprev))
194 return ((self.tipnode == repo.changelog.node(self.tiprev))
195 and (self.filteredhash == \
195 and (self.filteredhash == \
196 scmutil.filteredhash(repo, self.tiprev)))
196 scmutil.filteredhash(repo, self.tiprev)))
197 except IndexError:
197 except IndexError:
198 return False
198 return False
199
199
200 def _branchtip(self, heads):
200 def _branchtip(self, heads):
201 '''Return tuple with last open head in heads and false,
201 '''Return tuple with last open head in heads and false,
202 otherwise return last closed head and true.'''
202 otherwise return last closed head and true.'''
203 tip = heads[-1]
203 tip = heads[-1]
204 closed = True
204 closed = True
205 for h in reversed(heads):
205 for h in reversed(heads):
206 if h not in self._closednodes:
206 if h not in self._closednodes:
207 tip = h
207 tip = h
208 closed = False
208 closed = False
209 break
209 break
210 return tip, closed
210 return tip, closed
211
211
212 def branchtip(self, branch):
212 def branchtip(self, branch):
213 '''Return the tipmost open head on branch head, otherwise return the
213 '''Return the tipmost open head on branch head, otherwise return the
214 tipmost closed head on branch.
214 tipmost closed head on branch.
215 Raise KeyError for unknown branch.'''
215 Raise KeyError for unknown branch.'''
216 return self._branchtip(self[branch])[0]
216 return self._branchtip(self[branch])[0]
217
217
218 def branchheads(self, branch, closed=False):
218 def branchheads(self, branch, closed=False):
219 heads = self[branch]
219 heads = self[branch]
220 if not closed:
220 if not closed:
221 heads = [h for h in heads if h not in self._closednodes]
221 heads = [h for h in heads if h not in self._closednodes]
222 return heads
222 return heads
223
223
224 def iterbranches(self):
224 def iterbranches(self):
225 for bn, heads in self.iteritems():
225 for bn, heads in self.iteritems():
226 yield (bn, heads) + self._branchtip(heads)
226 yield (bn, heads) + self._branchtip(heads)
227
227
228 def copy(self):
228 def copy(self):
229 """return an deep copy of the branchcache object"""
229 """return an deep copy of the branchcache object"""
230 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
230 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
231 self._closednodes)
231 self._closednodes)
232
232
233 def write(self, repo):
233 def write(self, repo):
234 try:
234 try:
235 f = repo.vfs(_filename(repo), "w", atomictemp=True)
235 f = repo.vfs(_filename(repo), "w", atomictemp=True)
236 cachekey = [hex(self.tipnode), str(self.tiprev)]
236 cachekey = [hex(self.tipnode), str(self.tiprev)]
237 if self.filteredhash is not None:
237 if self.filteredhash is not None:
238 cachekey.append(hex(self.filteredhash))
238 cachekey.append(hex(self.filteredhash))
239 f.write(" ".join(cachekey) + '\n')
239 f.write(" ".join(cachekey) + '\n')
240 nodecount = 0
240 nodecount = 0
241 for label, nodes in sorted(self.iteritems()):
241 for label, nodes in sorted(self.iteritems()):
242 for node in nodes:
242 for node in nodes:
243 nodecount += 1
243 nodecount += 1
244 if node in self._closednodes:
244 if node in self._closednodes:
245 state = 'c'
245 state = 'c'
246 else:
246 else:
247 state = 'o'
247 state = 'o'
248 f.write("%s %s %s\n" % (hex(node), state,
248 f.write("%s %s %s\n" % (hex(node), state,
249 encoding.fromlocal(label)))
249 encoding.fromlocal(label)))
250 f.close()
250 f.close()
251 repo.ui.log('branchcache',
251 repo.ui.log('branchcache',
252 'wrote %s branch cache with %d labels and %d nodes\n',
252 'wrote %s branch cache with %d labels and %d nodes\n',
253 repo.filtername, len(self), nodecount)
253 repo.filtername, len(self), nodecount)
254 except (IOError, OSError, error.Abort) as inst:
254 except (IOError, OSError, error.Abort) as inst:
255 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
255 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
256 # Abort may be raise by read only opener
256 # Abort may be raise by read only opener
257 pass
257 pass
258
258
259 def update(self, repo, revgen):
259 def update(self, repo, revgen):
260 """Given a branchhead cache, self, that may have extra nodes or be
260 """Given a branchhead cache, self, that may have extra nodes or be
261 missing heads, and a generator of nodes that are strictly a superset of
261 missing heads, and a generator of nodes that are strictly a superset of
262 heads missing, this function updates self to be correct.
262 heads missing, this function updates self to be correct.
263 """
263 """
264 starttime = util.timer()
264 starttime = util.timer()
265 cl = repo.changelog
265 cl = repo.changelog
266 # collect new branch entries
266 # collect new branch entries
267 newbranches = {}
267 newbranches = {}
268 getbranchinfo = repo.revbranchcache().branchinfo
268 getbranchinfo = repo.revbranchcache().branchinfo
269 for r in revgen:
269 for r in revgen:
270 branch, closesbranch = getbranchinfo(r)
270 branch, closesbranch = getbranchinfo(r)
271 newbranches.setdefault(branch, []).append(r)
271 newbranches.setdefault(branch, []).append(r)
272 if closesbranch:
272 if closesbranch:
273 self._closednodes.add(cl.node(r))
273 self._closednodes.add(cl.node(r))
274
274
275 # fetch current topological heads to speed up filtering
275 # fetch current topological heads to speed up filtering
276 topoheads = set(cl.headrevs())
276 topoheads = set(cl.headrevs())
277
277
278 # if older branchheads are reachable from new ones, they aren't
278 # if older branchheads are reachable from new ones, they aren't
279 # really branchheads. Note checking parents is insufficient:
279 # really branchheads. Note checking parents is insufficient:
280 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
280 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
281 for branch, newheadrevs in newbranches.iteritems():
281 for branch, newheadrevs in newbranches.iteritems():
282 bheads = self.setdefault(branch, [])
282 bheads = self.setdefault(branch, [])
283 bheadset = set(cl.rev(node) for node in bheads)
283 bheadset = set(cl.rev(node) for node in bheads)
284
284
285 # This have been tested True on all internal usage of this function.
285 # This have been tested True on all internal usage of this function.
286 # run it again in case of doubt
286 # run it again in case of doubt
287 # assert not (set(bheadrevs) & set(newheadrevs))
287 # assert not (set(bheadrevs) & set(newheadrevs))
288 newheadrevs.sort()
288 newheadrevs.sort()
289 bheadset.update(newheadrevs)
289 bheadset.update(newheadrevs)
290
290
291 # This prunes out two kinds of heads - heads that are superseded by
291 # This prunes out two kinds of heads - heads that are superseded by
292 # a head in newheadrevs, and newheadrevs that are not heads because
292 # a head in newheadrevs, and newheadrevs that are not heads because
293 # an existing head is their descendant.
293 # an existing head is their descendant.
294 uncertain = bheadset - topoheads
294 uncertain = bheadset - topoheads
295 if uncertain:
295 if uncertain:
296 floorrev = min(uncertain)
296 floorrev = min(uncertain)
297 ancestors = set(cl.ancestors(newheadrevs, floorrev))
297 ancestors = set(cl.ancestors(newheadrevs, floorrev))
298 bheadset -= ancestors
298 bheadset -= ancestors
299 bheadrevs = sorted(bheadset)
299 bheadrevs = sorted(bheadset)
300 self[branch] = [cl.node(rev) for rev in bheadrevs]
300 self[branch] = [cl.node(rev) for rev in bheadrevs]
301 tiprev = bheadrevs[-1]
301 tiprev = bheadrevs[-1]
302 if tiprev > self.tiprev:
302 if tiprev > self.tiprev:
303 self.tipnode = cl.node(tiprev)
303 self.tipnode = cl.node(tiprev)
304 self.tiprev = tiprev
304 self.tiprev = tiprev
305
305
306 if not self.validfor(repo):
306 if not self.validfor(repo):
307 # cache key are not valid anymore
307 # cache key are not valid anymore
308 self.tipnode = nullid
308 self.tipnode = nullid
309 self.tiprev = nullrev
309 self.tiprev = nullrev
310 for heads in self.values():
310 for heads in self.values():
311 tiprev = max(cl.rev(node) for node in heads)
311 tiprev = max(cl.rev(node) for node in heads)
312 if tiprev > self.tiprev:
312 if tiprev > self.tiprev:
313 self.tipnode = cl.node(tiprev)
313 self.tipnode = cl.node(tiprev)
314 self.tiprev = tiprev
314 self.tiprev = tiprev
315 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
315 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
316
316
317 duration = util.timer() - starttime
317 duration = util.timer() - starttime
318 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
318 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
319 repo.filtername, duration)
319 repo.filtername, duration)
320
320
321 # Revision branch info cache
321 # Revision branch info cache
322
322
323 _rbcversion = '-v1'
323 _rbcversion = '-v1'
324 _rbcnames = 'cache/rbc-names' + _rbcversion
324 _rbcnames = 'cache/rbc-names' + _rbcversion
325 _rbcrevs = 'cache/rbc-revs' + _rbcversion
325 _rbcrevs = 'cache/rbc-revs' + _rbcversion
326 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
326 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
327 _rbcrecfmt = '>4sI'
327 _rbcrecfmt = '>4sI'
328 _rbcrecsize = calcsize(_rbcrecfmt)
328 _rbcrecsize = calcsize(_rbcrecfmt)
329 _rbcnodelen = 4
329 _rbcnodelen = 4
330 _rbcbranchidxmask = 0x7fffffff
330 _rbcbranchidxmask = 0x7fffffff
331 _rbccloseflag = 0x80000000
331 _rbccloseflag = 0x80000000
332
332
333 class revbranchcache(object):
333 class revbranchcache(object):
334 """Persistent cache, mapping from revision number to branch name and close.
334 """Persistent cache, mapping from revision number to branch name and close.
335 This is a low level cache, independent of filtering.
335 This is a low level cache, independent of filtering.
336
336
337 Branch names are stored in rbc-names in internal encoding separated by 0.
337 Branch names are stored in rbc-names in internal encoding separated by 0.
338 rbc-names is append-only, and each branch name is only stored once and will
338 rbc-names is append-only, and each branch name is only stored once and will
339 thus have a unique index.
339 thus have a unique index.
340
340
341 The branch info for each revision is stored in rbc-revs as constant size
341 The branch info for each revision is stored in rbc-revs as constant size
342 records. The whole file is read into memory, but it is only 'parsed' on
342 records. The whole file is read into memory, but it is only 'parsed' on
343 demand. The file is usually append-only but will be truncated if repo
343 demand. The file is usually append-only but will be truncated if repo
344 modification is detected.
344 modification is detected.
345 The record for each revision contains the first 4 bytes of the
345 The record for each revision contains the first 4 bytes of the
346 corresponding node hash, and the record is only used if it still matches.
346 corresponding node hash, and the record is only used if it still matches.
347 Even a completely trashed rbc-revs fill thus still give the right result
347 Even a completely trashed rbc-revs fill thus still give the right result
348 while converging towards full recovery ... assuming no incorrectly matching
348 while converging towards full recovery ... assuming no incorrectly matching
349 node hashes.
349 node hashes.
350 The record also contains 4 bytes where 31 bits contains the index of the
350 The record also contains 4 bytes where 31 bits contains the index of the
351 branch and the last bit indicate that it is a branch close commit.
351 branch and the last bit indicate that it is a branch close commit.
352 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
352 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
353 and will grow with it but be 1/8th of its size.
353 and will grow with it but be 1/8th of its size.
354 """
354 """
355
355
356 def __init__(self, repo, readonly=True):
356 def __init__(self, repo, readonly=True):
357 assert repo.filtername is None
357 assert repo.filtername is None
358 self._repo = repo
358 self._repo = repo
359 self._names = [] # branch names in local encoding with static index
359 self._names = [] # branch names in local encoding with static index
360 self._rbcrevs = bytearray()
360 self._rbcrevs = bytearray()
361 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
361 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
362 try:
362 try:
363 bndata = repo.vfs.read(_rbcnames)
363 bndata = repo.vfs.read(_rbcnames)
364 self._rbcsnameslen = len(bndata) # for verification before writing
364 self._rbcsnameslen = len(bndata) # for verification before writing
365 self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')]
365 self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')]
366 except (IOError, OSError):
366 except (IOError, OSError):
367 if readonly:
367 if readonly:
368 # don't try to use cache - fall back to the slow path
368 # don't try to use cache - fall back to the slow path
369 self.branchinfo = self._branchinfo
369 self.branchinfo = self._branchinfo
370
370
371 if self._names:
371 if self._names:
372 try:
372 try:
373 data = repo.vfs.read(_rbcrevs)
373 data = repo.vfs.read(_rbcrevs)
374 self._rbcrevs[:] = data
374 self._rbcrevs[:] = data
375 except (IOError, OSError) as inst:
375 except (IOError, OSError) as inst:
376 repo.ui.debug("couldn't read revision branch cache: %s\n" %
376 repo.ui.debug("couldn't read revision branch cache: %s\n" %
377 inst)
377 inst)
378 # remember number of good records on disk
378 # remember number of good records on disk
379 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
379 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
380 len(repo.changelog))
380 len(repo.changelog))
381 if self._rbcrevslen == 0:
381 if self._rbcrevslen == 0:
382 self._names = []
382 self._names = []
383 self._rbcnamescount = len(self._names) # number of names read at
383 self._rbcnamescount = len(self._names) # number of names read at
384 # _rbcsnameslen
384 # _rbcsnameslen
385 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
385 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
386
386
387 def _clear(self):
387 def _clear(self):
388 self._rbcsnameslen = 0
388 self._rbcsnameslen = 0
389 del self._names[:]
389 del self._names[:]
390 self._rbcnamescount = 0
390 self._rbcnamescount = 0
391 self._namesreverse.clear()
391 self._namesreverse.clear()
392 self._rbcrevslen = len(self._repo.changelog)
392 self._rbcrevslen = len(self._repo.changelog)
393 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
393 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
394
394
395 def branchinfo(self, rev):
395 def branchinfo(self, rev):
396 """Return branch name and close flag for rev, using and updating
396 """Return branch name and close flag for rev, using and updating
397 persistent cache."""
397 persistent cache."""
398 changelog = self._repo.changelog
398 changelog = self._repo.changelog
399 rbcrevidx = rev * _rbcrecsize
399 rbcrevidx = rev * _rbcrecsize
400
400
401 # avoid negative index, changelog.read(nullrev) is fast without cache
401 # avoid negative index, changelog.read(nullrev) is fast without cache
402 if rev == nullrev:
402 if rev == nullrev:
403 return changelog.branchinfo(rev)
403 return changelog.branchinfo(rev)
404
404
405 # if requested rev isn't allocated, grow and cache the rev info
405 # if requested rev isn't allocated, grow and cache the rev info
406 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
406 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
407 return self._branchinfo(rev)
407 return self._branchinfo(rev)
408
408
409 # fast path: extract data from cache, use it if node is matching
409 # fast path: extract data from cache, use it if node is matching
410 reponode = changelog.node(rev)[:_rbcnodelen]
410 reponode = changelog.node(rev)[:_rbcnodelen]
411 cachenode, branchidx = unpack(
411 cachenode, branchidx = unpack(
412 _rbcrecfmt, buffer(self._rbcrevs, rbcrevidx, _rbcrecsize))
412 _rbcrecfmt, util.buffer(self._rbcrevs, rbcrevidx, _rbcrecsize))
413 close = bool(branchidx & _rbccloseflag)
413 close = bool(branchidx & _rbccloseflag)
414 if close:
414 if close:
415 branchidx &= _rbcbranchidxmask
415 branchidx &= _rbcbranchidxmask
416 if cachenode == '\0\0\0\0':
416 if cachenode == '\0\0\0\0':
417 pass
417 pass
418 elif cachenode == reponode:
418 elif cachenode == reponode:
419 try:
419 try:
420 return self._names[branchidx], close
420 return self._names[branchidx], close
421 except IndexError:
421 except IndexError:
422 # recover from invalid reference to unknown branch
422 # recover from invalid reference to unknown branch
423 self._repo.ui.debug("referenced branch names not found"
423 self._repo.ui.debug("referenced branch names not found"
424 " - rebuilding revision branch cache from scratch\n")
424 " - rebuilding revision branch cache from scratch\n")
425 self._clear()
425 self._clear()
426 else:
426 else:
427 # rev/node map has changed, invalidate the cache from here up
427 # rev/node map has changed, invalidate the cache from here up
428 self._repo.ui.debug("history modification detected - truncating "
428 self._repo.ui.debug("history modification detected - truncating "
429 "revision branch cache to revision %s\n" % rev)
429 "revision branch cache to revision %s\n" % rev)
430 truncate = rbcrevidx + _rbcrecsize
430 truncate = rbcrevidx + _rbcrecsize
431 del self._rbcrevs[truncate:]
431 del self._rbcrevs[truncate:]
432 self._rbcrevslen = min(self._rbcrevslen, truncate)
432 self._rbcrevslen = min(self._rbcrevslen, truncate)
433
433
434 # fall back to slow path and make sure it will be written to disk
434 # fall back to slow path and make sure it will be written to disk
435 return self._branchinfo(rev)
435 return self._branchinfo(rev)
436
436
437 def _branchinfo(self, rev):
437 def _branchinfo(self, rev):
438 """Retrieve branch info from changelog and update _rbcrevs"""
438 """Retrieve branch info from changelog and update _rbcrevs"""
439 changelog = self._repo.changelog
439 changelog = self._repo.changelog
440 b, close = changelog.branchinfo(rev)
440 b, close = changelog.branchinfo(rev)
441 if b in self._namesreverse:
441 if b in self._namesreverse:
442 branchidx = self._namesreverse[b]
442 branchidx = self._namesreverse[b]
443 else:
443 else:
444 branchidx = len(self._names)
444 branchidx = len(self._names)
445 self._names.append(b)
445 self._names.append(b)
446 self._namesreverse[b] = branchidx
446 self._namesreverse[b] = branchidx
447 reponode = changelog.node(rev)
447 reponode = changelog.node(rev)
448 if close:
448 if close:
449 branchidx |= _rbccloseflag
449 branchidx |= _rbccloseflag
450 self._setcachedata(rev, reponode, branchidx)
450 self._setcachedata(rev, reponode, branchidx)
451 return b, close
451 return b, close
452
452
453 def _setcachedata(self, rev, node, branchidx):
453 def _setcachedata(self, rev, node, branchidx):
454 """Writes the node's branch data to the in-memory cache data."""
454 """Writes the node's branch data to the in-memory cache data."""
455 rbcrevidx = rev * _rbcrecsize
455 rbcrevidx = rev * _rbcrecsize
456 rec = bytearray(pack(_rbcrecfmt, node, branchidx))
456 rec = bytearray(pack(_rbcrecfmt, node, branchidx))
457 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
457 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
458 self._rbcrevs.extend('\0' *
458 self._rbcrevs.extend('\0' *
459 (len(self._repo.changelog) * _rbcrecsize -
459 (len(self._repo.changelog) * _rbcrecsize -
460 len(self._rbcrevs)))
460 len(self._rbcrevs)))
461 self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec
461 self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec
462 self._rbcrevslen = min(self._rbcrevslen, rev)
462 self._rbcrevslen = min(self._rbcrevslen, rev)
463
463
464 tr = self._repo.currenttransaction()
464 tr = self._repo.currenttransaction()
465 if tr:
465 if tr:
466 tr.addfinalize('write-revbranchcache', self.write)
466 tr.addfinalize('write-revbranchcache', self.write)
467
467
468 def write(self, tr=None):
468 def write(self, tr=None):
469 """Save branch cache if it is dirty."""
469 """Save branch cache if it is dirty."""
470 repo = self._repo
470 repo = self._repo
471 wlock = None
471 wlock = None
472 step = ''
472 step = ''
473 try:
473 try:
474 if self._rbcnamescount < len(self._names):
474 if self._rbcnamescount < len(self._names):
475 step = ' names'
475 step = ' names'
476 wlock = repo.wlock(wait=False)
476 wlock = repo.wlock(wait=False)
477 if self._rbcnamescount != 0:
477 if self._rbcnamescount != 0:
478 f = repo.vfs.open(_rbcnames, 'ab')
478 f = repo.vfs.open(_rbcnames, 'ab')
479 if f.tell() == self._rbcsnameslen:
479 if f.tell() == self._rbcsnameslen:
480 f.write('\0')
480 f.write('\0')
481 else:
481 else:
482 f.close()
482 f.close()
483 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
483 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
484 self._rbcnamescount = 0
484 self._rbcnamescount = 0
485 self._rbcrevslen = 0
485 self._rbcrevslen = 0
486 if self._rbcnamescount == 0:
486 if self._rbcnamescount == 0:
487 # before rewriting names, make sure references are removed
487 # before rewriting names, make sure references are removed
488 repo.vfs.unlinkpath(_rbcrevs, ignoremissing=True)
488 repo.vfs.unlinkpath(_rbcrevs, ignoremissing=True)
489 f = repo.vfs.open(_rbcnames, 'wb')
489 f = repo.vfs.open(_rbcnames, 'wb')
490 f.write('\0'.join(encoding.fromlocal(b)
490 f.write('\0'.join(encoding.fromlocal(b)
491 for b in self._names[self._rbcnamescount:]))
491 for b in self._names[self._rbcnamescount:]))
492 self._rbcsnameslen = f.tell()
492 self._rbcsnameslen = f.tell()
493 f.close()
493 f.close()
494 self._rbcnamescount = len(self._names)
494 self._rbcnamescount = len(self._names)
495
495
496 start = self._rbcrevslen * _rbcrecsize
496 start = self._rbcrevslen * _rbcrecsize
497 if start != len(self._rbcrevs):
497 if start != len(self._rbcrevs):
498 step = ''
498 step = ''
499 if wlock is None:
499 if wlock is None:
500 wlock = repo.wlock(wait=False)
500 wlock = repo.wlock(wait=False)
501 revs = min(len(repo.changelog),
501 revs = min(len(repo.changelog),
502 len(self._rbcrevs) // _rbcrecsize)
502 len(self._rbcrevs) // _rbcrecsize)
503 f = repo.vfs.open(_rbcrevs, 'ab')
503 f = repo.vfs.open(_rbcrevs, 'ab')
504 if f.tell() != start:
504 if f.tell() != start:
505 repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start))
505 repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start))
506 f.seek(start)
506 f.seek(start)
507 if f.tell() != start:
507 if f.tell() != start:
508 start = 0
508 start = 0
509 f.seek(start)
509 f.seek(start)
510 f.truncate()
510 f.truncate()
511 end = revs * _rbcrecsize
511 end = revs * _rbcrecsize
512 f.write(self._rbcrevs[start:end])
512 f.write(self._rbcrevs[start:end])
513 f.close()
513 f.close()
514 self._rbcrevslen = revs
514 self._rbcrevslen = revs
515 except (IOError, OSError, error.Abort, error.LockError) as inst:
515 except (IOError, OSError, error.Abort, error.LockError) as inst:
516 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
516 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
517 % (step, inst))
517 % (step, inst))
518 finally:
518 finally:
519 if wlock is not None:
519 if wlock is not None:
520 wlock.release()
520 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now