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