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