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