##// END OF EJS Templates
branchmap: annotate constructor type for branchcache...
Augie Fackler -
r44035:9c1eccdd default
parent child Browse files
Show More
@@ -1,738 +1,756 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 repoviewutil,
26 repoviewutil,
27 stringutil,
27 stringutil,
28 )
28 )
29
29
30 if not globals():
31 from typing import (
32 Any,
33 Callable,
34 Dict,
35 Iterable,
36 List,
37 Optional,
38 Set,
39 Tuple,
40 Union,
41 )
42
43 assert any(
44 (Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union,)
45 )
46
30 subsettable = repoviewutil.subsettable
47 subsettable = repoviewutil.subsettable
31
48
32 calcsize = struct.calcsize
49 calcsize = struct.calcsize
33 pack_into = struct.pack_into
50 pack_into = struct.pack_into
34 unpack_from = struct.unpack_from
51 unpack_from = struct.unpack_from
35
52
36
53
37 class BranchMapCache(object):
54 class BranchMapCache(object):
38 """mapping of filtered views of repo with their branchcache"""
55 """mapping of filtered views of repo with their branchcache"""
39
56
40 def __init__(self):
57 def __init__(self):
41 self._per_filter = {}
58 self._per_filter = {}
42
59
43 def __getitem__(self, repo):
60 def __getitem__(self, repo):
44 self.updatecache(repo)
61 self.updatecache(repo)
45 return self._per_filter[repo.filtername]
62 return self._per_filter[repo.filtername]
46
63
47 def updatecache(self, repo):
64 def updatecache(self, repo):
48 """Update the cache for the given filtered view on a repository"""
65 """Update the cache for the given filtered view on a repository"""
49 # This can trigger updates for the caches for subsets of the filtered
66 # This can trigger updates for the caches for subsets of the filtered
50 # view, e.g. when there is no cache for this filtered view or the cache
67 # view, e.g. when there is no cache for this filtered view or the cache
51 # is stale.
68 # is stale.
52
69
53 cl = repo.changelog
70 cl = repo.changelog
54 filtername = repo.filtername
71 filtername = repo.filtername
55 bcache = self._per_filter.get(filtername)
72 bcache = self._per_filter.get(filtername)
56 if bcache is None or not bcache.validfor(repo):
73 if bcache is None or not bcache.validfor(repo):
57 # cache object missing or cache object stale? Read from disk
74 # cache object missing or cache object stale? Read from disk
58 bcache = branchcache.fromfile(repo)
75 bcache = branchcache.fromfile(repo)
59
76
60 revs = []
77 revs = []
61 if bcache is None:
78 if bcache is None:
62 # no (fresh) cache available anymore, perhaps we can re-use
79 # no (fresh) cache available anymore, perhaps we can re-use
63 # the cache for a subset, then extend that to add info on missing
80 # the cache for a subset, then extend that to add info on missing
64 # revisions.
81 # revisions.
65 subsetname = subsettable.get(filtername)
82 subsetname = subsettable.get(filtername)
66 if subsetname is not None:
83 if subsetname is not None:
67 subset = repo.filtered(subsetname)
84 subset = repo.filtered(subsetname)
68 bcache = self[subset].copy()
85 bcache = self[subset].copy()
69 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
86 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
70 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
87 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
71 else:
88 else:
72 # nothing to fall back on, start empty.
89 # nothing to fall back on, start empty.
73 bcache = branchcache()
90 bcache = branchcache()
74
91
75 revs.extend(cl.revs(start=bcache.tiprev + 1))
92 revs.extend(cl.revs(start=bcache.tiprev + 1))
76 if revs:
93 if revs:
77 bcache.update(repo, revs)
94 bcache.update(repo, revs)
78
95
79 assert bcache.validfor(repo), filtername
96 assert bcache.validfor(repo), filtername
80 self._per_filter[repo.filtername] = bcache
97 self._per_filter[repo.filtername] = bcache
81
98
82 def replace(self, repo, remotebranchmap):
99 def replace(self, repo, remotebranchmap):
83 """Replace the branchmap cache for a repo with a branch mapping.
100 """Replace the branchmap cache for a repo with a branch mapping.
84
101
85 This is likely only called during clone with a branch map from a
102 This is likely only called during clone with a branch map from a
86 remote.
103 remote.
87
104
88 """
105 """
89 cl = repo.changelog
106 cl = repo.changelog
90 clrev = cl.rev
107 clrev = cl.rev
91 clbranchinfo = cl.branchinfo
108 clbranchinfo = cl.branchinfo
92 rbheads = []
109 rbheads = []
93 closed = []
110 closed = []
94 for bheads in pycompat.itervalues(remotebranchmap):
111 for bheads in pycompat.itervalues(remotebranchmap):
95 rbheads += bheads
112 rbheads += bheads
96 for h in bheads:
113 for h in bheads:
97 r = clrev(h)
114 r = clrev(h)
98 b, c = clbranchinfo(r)
115 b, c = clbranchinfo(r)
99 if c:
116 if c:
100 closed.append(h)
117 closed.append(h)
101
118
102 if rbheads:
119 if rbheads:
103 rtiprev = max((int(clrev(node)) for node in rbheads))
120 rtiprev = max((int(clrev(node)) for node in rbheads))
104 cache = branchcache(
121 cache = branchcache(
105 remotebranchmap,
122 remotebranchmap,
106 repo[rtiprev].node(),
123 repo[rtiprev].node(),
107 rtiprev,
124 rtiprev,
108 closednodes=set(closed),
125 closednodes=set(closed),
109 )
126 )
110
127
111 # Try to stick it as low as possible
128 # Try to stick it as low as possible
112 # filter above served are unlikely to be fetch from a clone
129 # filter above served are unlikely to be fetch from a clone
113 for candidate in (b'base', b'immutable', b'served'):
130 for candidate in (b'base', b'immutable', b'served'):
114 rview = repo.filtered(candidate)
131 rview = repo.filtered(candidate)
115 if cache.validfor(rview):
132 if cache.validfor(rview):
116 self._per_filter[candidate] = cache
133 self._per_filter[candidate] = cache
117 cache.write(rview)
134 cache.write(rview)
118 return
135 return
119
136
120 def clear(self):
137 def clear(self):
121 self._per_filter.clear()
138 self._per_filter.clear()
122
139
123
140
124 def _unknownnode(node):
141 def _unknownnode(node):
125 """ raises ValueError when branchcache found a node which does not exists
142 """ raises ValueError when branchcache found a node which does not exists
126 """
143 """
127 raise ValueError('node %s does not exist' % pycompat.sysstr(hex(node)))
144 raise ValueError('node %s does not exist' % pycompat.sysstr(hex(node)))
128
145
129
146
130 def _branchcachedesc(repo):
147 def _branchcachedesc(repo):
131 if repo.filtername is not None:
148 if repo.filtername is not None:
132 return b'branch cache (%s)' % repo.filtername
149 return b'branch cache (%s)' % repo.filtername
133 else:
150 else:
134 return b'branch cache'
151 return b'branch cache'
135
152
136
153
137 class branchcache(object):
154 class branchcache(object):
138 """A dict like object that hold branches heads cache.
155 """A dict like object that hold branches heads cache.
139
156
140 This cache is used to avoid costly computations to determine all the
157 This cache is used to avoid costly computations to determine all the
141 branch heads of a repo.
158 branch heads of a repo.
142
159
143 The cache is serialized on disk in the following format:
160 The cache is serialized on disk in the following format:
144
161
145 <tip hex node> <tip rev number> [optional filtered repo hex hash]
162 <tip hex node> <tip rev number> [optional filtered repo hex hash]
146 <branch head hex node> <open/closed state> <branch name>
163 <branch head hex node> <open/closed state> <branch name>
147 <branch head hex node> <open/closed state> <branch name>
164 <branch head hex node> <open/closed state> <branch name>
148 ...
165 ...
149
166
150 The first line is used to check if the cache is still valid. If the
167 The first line is used to check if the cache is still valid. If the
151 branch cache is for a filtered repo view, an optional third hash is
168 branch cache is for a filtered repo view, an optional third hash is
152 included that hashes the hashes of all filtered revisions.
169 included that hashes the hashes of all filtered revisions.
153
170
154 The open/closed state is represented by a single letter 'o' or 'c'.
171 The open/closed state is represented by a single letter 'o' or 'c'.
155 This field can be used to avoid changelog reads when determining if a
172 This field can be used to avoid changelog reads when determining if a
156 branch head closes a branch or not.
173 branch head closes a branch or not.
157 """
174 """
158
175
159 def __init__(
176 def __init__(
160 self,
177 self,
161 entries=(),
178 entries=(),
162 tipnode=nullid,
179 tipnode=nullid,
163 tiprev=nullrev,
180 tiprev=nullrev,
164 filteredhash=None,
181 filteredhash=None,
165 closednodes=None,
182 closednodes=None,
166 hasnode=None,
183 hasnode=None,
167 ):
184 ):
185 # type: (Union[Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]], bytes, int, Optional[bytes], Optional[Set[bytes]], Optional[Callable[[bytes], bool]]) -> None
168 """ hasnode is a function which can be used to verify whether changelog
186 """ hasnode is a function which can be used to verify whether changelog
169 has a given node or not. If it's not provided, we assume that every node
187 has a given node or not. If it's not provided, we assume that every node
170 we have exists in changelog """
188 we have exists in changelog """
171 self.tipnode = tipnode
189 self.tipnode = tipnode
172 self.tiprev = tiprev
190 self.tiprev = tiprev
173 self.filteredhash = filteredhash
191 self.filteredhash = filteredhash
174 # closednodes is a set of nodes that close their branch. If the branch
192 # closednodes is a set of nodes that close their branch. If the branch
175 # cache has been updated, it may contain nodes that are no longer
193 # cache has been updated, it may contain nodes that are no longer
176 # heads.
194 # heads.
177 if closednodes is None:
195 if closednodes is None:
178 self._closednodes = set()
196 self._closednodes = set()
179 else:
197 else:
180 self._closednodes = set(closednodes)
198 self._closednodes = closednodes
181 self._entries = dict(entries)
199 self._entries = dict(entries)
182 # whether closed nodes are verified or not
200 # whether closed nodes are verified or not
183 self._closedverified = False
201 self._closedverified = False
184 # branches for which nodes are verified
202 # branches for which nodes are verified
185 self._verifiedbranches = set()
203 self._verifiedbranches = set()
186 self._hasnode = hasnode
204 self._hasnode = hasnode
187 if self._hasnode is None:
205 if self._hasnode is None:
188 self._hasnode = lambda x: True
206 self._hasnode = lambda x: True
189
207
190 def _verifyclosed(self):
208 def _verifyclosed(self):
191 """ verify the closed nodes we have """
209 """ verify the closed nodes we have """
192 if self._closedverified:
210 if self._closedverified:
193 return
211 return
194 for node in self._closednodes:
212 for node in self._closednodes:
195 if not self._hasnode(node):
213 if not self._hasnode(node):
196 _unknownnode(node)
214 _unknownnode(node)
197
215
198 self._closedverified = True
216 self._closedverified = True
199
217
200 def _verifybranch(self, branch):
218 def _verifybranch(self, branch):
201 """ verify head nodes for the given branch. """
219 """ verify head nodes for the given branch. """
202 if branch not in self._entries or branch in self._verifiedbranches:
220 if branch not in self._entries or branch in self._verifiedbranches:
203 return
221 return
204 for n in self._entries[branch]:
222 for n in self._entries[branch]:
205 if not self._hasnode(n):
223 if not self._hasnode(n):
206 _unknownnode(n)
224 _unknownnode(n)
207
225
208 self._verifiedbranches.add(branch)
226 self._verifiedbranches.add(branch)
209
227
210 def _verifyall(self):
228 def _verifyall(self):
211 """ verifies nodes of all the branches """
229 """ verifies nodes of all the branches """
212 needverification = set(self._entries.keys()) - self._verifiedbranches
230 needverification = set(self._entries.keys()) - self._verifiedbranches
213 for b in needverification:
231 for b in needverification:
214 self._verifybranch(b)
232 self._verifybranch(b)
215
233
216 def __iter__(self):
234 def __iter__(self):
217 return iter(self._entries)
235 return iter(self._entries)
218
236
219 def __setitem__(self, key, value):
237 def __setitem__(self, key, value):
220 self._entries[key] = value
238 self._entries[key] = value
221
239
222 def __getitem__(self, key):
240 def __getitem__(self, key):
223 self._verifybranch(key)
241 self._verifybranch(key)
224 return self._entries[key]
242 return self._entries[key]
225
243
226 def __contains__(self, key):
244 def __contains__(self, key):
227 self._verifybranch(key)
245 self._verifybranch(key)
228 return key in self._entries
246 return key in self._entries
229
247
230 def iteritems(self):
248 def iteritems(self):
231 for k, v in pycompat.iteritems(self._entries):
249 for k, v in pycompat.iteritems(self._entries):
232 self._verifybranch(k)
250 self._verifybranch(k)
233 yield k, v
251 yield k, v
234
252
235 items = iteritems
253 items = iteritems
236
254
237 def hasbranch(self, label):
255 def hasbranch(self, label):
238 """ checks whether a branch of this name exists or not """
256 """ checks whether a branch of this name exists or not """
239 self._verifybranch(label)
257 self._verifybranch(label)
240 return label in self._entries
258 return label in self._entries
241
259
242 @classmethod
260 @classmethod
243 def fromfile(cls, repo):
261 def fromfile(cls, repo):
244 f = None
262 f = None
245 try:
263 try:
246 f = repo.cachevfs(cls._filename(repo))
264 f = repo.cachevfs(cls._filename(repo))
247 lineiter = iter(f)
265 lineiter = iter(f)
248 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
266 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
249 last, lrev = cachekey[:2]
267 last, lrev = cachekey[:2]
250 last, lrev = bin(last), int(lrev)
268 last, lrev = bin(last), int(lrev)
251 filteredhash = None
269 filteredhash = None
252 hasnode = repo.changelog.hasnode
270 hasnode = repo.changelog.hasnode
253 if len(cachekey) > 2:
271 if len(cachekey) > 2:
254 filteredhash = bin(cachekey[2])
272 filteredhash = bin(cachekey[2])
255 bcache = cls(
273 bcache = cls(
256 tipnode=last,
274 tipnode=last,
257 tiprev=lrev,
275 tiprev=lrev,
258 filteredhash=filteredhash,
276 filteredhash=filteredhash,
259 hasnode=hasnode,
277 hasnode=hasnode,
260 )
278 )
261 if not bcache.validfor(repo):
279 if not bcache.validfor(repo):
262 # invalidate the cache
280 # invalidate the cache
263 raise ValueError('tip differs')
281 raise ValueError('tip differs')
264 bcache.load(repo, lineiter)
282 bcache.load(repo, lineiter)
265 except (IOError, OSError):
283 except (IOError, OSError):
266 return None
284 return None
267
285
268 except Exception as inst:
286 except Exception as inst:
269 if repo.ui.debugflag:
287 if repo.ui.debugflag:
270 msg = b'invalid %s: %s\n'
288 msg = b'invalid %s: %s\n'
271 repo.ui.debug(
289 repo.ui.debug(
272 msg
290 msg
273 % (
291 % (
274 _branchcachedesc(repo),
292 _branchcachedesc(repo),
275 pycompat.bytestr(
293 pycompat.bytestr(
276 inst # pytype: disable=wrong-arg-types
294 inst # pytype: disable=wrong-arg-types
277 ),
295 ),
278 )
296 )
279 )
297 )
280 bcache = None
298 bcache = None
281
299
282 finally:
300 finally:
283 if f:
301 if f:
284 f.close()
302 f.close()
285
303
286 return bcache
304 return bcache
287
305
288 def load(self, repo, lineiter):
306 def load(self, repo, lineiter):
289 """ fully loads the branchcache by reading from the file using the line
307 """ fully loads the branchcache by reading from the file using the line
290 iterator passed"""
308 iterator passed"""
291 for line in lineiter:
309 for line in lineiter:
292 line = line.rstrip(b'\n')
310 line = line.rstrip(b'\n')
293 if not line:
311 if not line:
294 continue
312 continue
295 node, state, label = line.split(b" ", 2)
313 node, state, label = line.split(b" ", 2)
296 if state not in b'oc':
314 if state not in b'oc':
297 raise ValueError('invalid branch state')
315 raise ValueError('invalid branch state')
298 label = encoding.tolocal(label.strip())
316 label = encoding.tolocal(label.strip())
299 node = bin(node)
317 node = bin(node)
300 self._entries.setdefault(label, []).append(node)
318 self._entries.setdefault(label, []).append(node)
301 if state == b'c':
319 if state == b'c':
302 self._closednodes.add(node)
320 self._closednodes.add(node)
303
321
304 @staticmethod
322 @staticmethod
305 def _filename(repo):
323 def _filename(repo):
306 """name of a branchcache file for a given repo or repoview"""
324 """name of a branchcache file for a given repo or repoview"""
307 filename = b"branch2"
325 filename = b"branch2"
308 if repo.filtername:
326 if repo.filtername:
309 filename = b'%s-%s' % (filename, repo.filtername)
327 filename = b'%s-%s' % (filename, repo.filtername)
310 return filename
328 return filename
311
329
312 def validfor(self, repo):
330 def validfor(self, repo):
313 """Is the cache content valid regarding a repo
331 """Is the cache content valid regarding a repo
314
332
315 - False when cached tipnode is unknown or if we detect a strip.
333 - False when cached tipnode is unknown or if we detect a strip.
316 - True when cache is up to date or a subset of current repo."""
334 - True when cache is up to date or a subset of current repo."""
317 try:
335 try:
318 return (self.tipnode == repo.changelog.node(self.tiprev)) and (
336 return (self.tipnode == repo.changelog.node(self.tiprev)) and (
319 self.filteredhash == scmutil.filteredhash(repo, self.tiprev)
337 self.filteredhash == scmutil.filteredhash(repo, self.tiprev)
320 )
338 )
321 except IndexError:
339 except IndexError:
322 return False
340 return False
323
341
324 def _branchtip(self, heads):
342 def _branchtip(self, heads):
325 '''Return tuple with last open head in heads and false,
343 '''Return tuple with last open head in heads and false,
326 otherwise return last closed head and true.'''
344 otherwise return last closed head and true.'''
327 tip = heads[-1]
345 tip = heads[-1]
328 closed = True
346 closed = True
329 for h in reversed(heads):
347 for h in reversed(heads):
330 if h not in self._closednodes:
348 if h not in self._closednodes:
331 tip = h
349 tip = h
332 closed = False
350 closed = False
333 break
351 break
334 return tip, closed
352 return tip, closed
335
353
336 def branchtip(self, branch):
354 def branchtip(self, branch):
337 '''Return the tipmost open head on branch head, otherwise return the
355 '''Return the tipmost open head on branch head, otherwise return the
338 tipmost closed head on branch.
356 tipmost closed head on branch.
339 Raise KeyError for unknown branch.'''
357 Raise KeyError for unknown branch.'''
340 return self._branchtip(self[branch])[0]
358 return self._branchtip(self[branch])[0]
341
359
342 def iteropen(self, nodes):
360 def iteropen(self, nodes):
343 return (n for n in nodes if n not in self._closednodes)
361 return (n for n in nodes if n not in self._closednodes)
344
362
345 def branchheads(self, branch, closed=False):
363 def branchheads(self, branch, closed=False):
346 self._verifybranch(branch)
364 self._verifybranch(branch)
347 heads = self._entries[branch]
365 heads = self._entries[branch]
348 if not closed:
366 if not closed:
349 heads = list(self.iteropen(heads))
367 heads = list(self.iteropen(heads))
350 return heads
368 return heads
351
369
352 def iterbranches(self):
370 def iterbranches(self):
353 for bn, heads in pycompat.iteritems(self):
371 for bn, heads in pycompat.iteritems(self):
354 yield (bn, heads) + self._branchtip(heads)
372 yield (bn, heads) + self._branchtip(heads)
355
373
356 def iterheads(self):
374 def iterheads(self):
357 """ returns all the heads """
375 """ returns all the heads """
358 self._verifyall()
376 self._verifyall()
359 return pycompat.itervalues(self._entries)
377 return pycompat.itervalues(self._entries)
360
378
361 def copy(self):
379 def copy(self):
362 """return an deep copy of the branchcache object"""
380 """return an deep copy of the branchcache object"""
363 return type(self)(
381 return type(self)(
364 self._entries,
382 self._entries,
365 self.tipnode,
383 self.tipnode,
366 self.tiprev,
384 self.tiprev,
367 self.filteredhash,
385 self.filteredhash,
368 self._closednodes,
386 self._closednodes,
369 )
387 )
370
388
371 def write(self, repo):
389 def write(self, repo):
372 try:
390 try:
373 f = repo.cachevfs(self._filename(repo), b"w", atomictemp=True)
391 f = repo.cachevfs(self._filename(repo), b"w", atomictemp=True)
374 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
392 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
375 if self.filteredhash is not None:
393 if self.filteredhash is not None:
376 cachekey.append(hex(self.filteredhash))
394 cachekey.append(hex(self.filteredhash))
377 f.write(b" ".join(cachekey) + b'\n')
395 f.write(b" ".join(cachekey) + b'\n')
378 nodecount = 0
396 nodecount = 0
379 for label, nodes in sorted(pycompat.iteritems(self._entries)):
397 for label, nodes in sorted(pycompat.iteritems(self._entries)):
380 label = encoding.fromlocal(label)
398 label = encoding.fromlocal(label)
381 for node in nodes:
399 for node in nodes:
382 nodecount += 1
400 nodecount += 1
383 if node in self._closednodes:
401 if node in self._closednodes:
384 state = b'c'
402 state = b'c'
385 else:
403 else:
386 state = b'o'
404 state = b'o'
387 f.write(b"%s %s %s\n" % (hex(node), state, label))
405 f.write(b"%s %s %s\n" % (hex(node), state, label))
388 f.close()
406 f.close()
389 repo.ui.log(
407 repo.ui.log(
390 b'branchcache',
408 b'branchcache',
391 b'wrote %s with %d labels and %d nodes\n',
409 b'wrote %s with %d labels and %d nodes\n',
392 _branchcachedesc(repo),
410 _branchcachedesc(repo),
393 len(self._entries),
411 len(self._entries),
394 nodecount,
412 nodecount,
395 )
413 )
396 except (IOError, OSError, error.Abort) as inst:
414 except (IOError, OSError, error.Abort) as inst:
397 # Abort may be raised by read only opener, so log and continue
415 # Abort may be raised by read only opener, so log and continue
398 repo.ui.debug(
416 repo.ui.debug(
399 b"couldn't write branch cache: %s\n"
417 b"couldn't write branch cache: %s\n"
400 % stringutil.forcebytestr(inst)
418 % stringutil.forcebytestr(inst)
401 )
419 )
402
420
403 def update(self, repo, revgen):
421 def update(self, repo, revgen):
404 """Given a branchhead cache, self, that may have extra nodes or be
422 """Given a branchhead cache, self, that may have extra nodes or be
405 missing heads, and a generator of nodes that are strictly a superset of
423 missing heads, and a generator of nodes that are strictly a superset of
406 heads missing, this function updates self to be correct.
424 heads missing, this function updates self to be correct.
407 """
425 """
408 starttime = util.timer()
426 starttime = util.timer()
409 cl = repo.changelog
427 cl = repo.changelog
410 # collect new branch entries
428 # collect new branch entries
411 newbranches = {}
429 newbranches = {}
412 getbranchinfo = repo.revbranchcache().branchinfo
430 getbranchinfo = repo.revbranchcache().branchinfo
413 for r in revgen:
431 for r in revgen:
414 branch, closesbranch = getbranchinfo(r)
432 branch, closesbranch = getbranchinfo(r)
415 newbranches.setdefault(branch, []).append(r)
433 newbranches.setdefault(branch, []).append(r)
416 if closesbranch:
434 if closesbranch:
417 self._closednodes.add(cl.node(r))
435 self._closednodes.add(cl.node(r))
418
436
419 # fetch current topological heads to speed up filtering
437 # fetch current topological heads to speed up filtering
420 topoheads = set(cl.headrevs())
438 topoheads = set(cl.headrevs())
421
439
422 # new tip revision which we found after iterating items from new
440 # new tip revision which we found after iterating items from new
423 # branches
441 # branches
424 ntiprev = self.tiprev
442 ntiprev = self.tiprev
425
443
426 # if older branchheads are reachable from new ones, they aren't
444 # if older branchheads are reachable from new ones, they aren't
427 # really branchheads. Note checking parents is insufficient:
445 # really branchheads. Note checking parents is insufficient:
428 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
446 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
429 for branch, newheadrevs in pycompat.iteritems(newbranches):
447 for branch, newheadrevs in pycompat.iteritems(newbranches):
430 bheads = self._entries.setdefault(branch, [])
448 bheads = self._entries.setdefault(branch, [])
431 bheadset = set(cl.rev(node) for node in bheads)
449 bheadset = set(cl.rev(node) for node in bheads)
432
450
433 # This have been tested True on all internal usage of this function.
451 # This have been tested True on all internal usage of this function.
434 # run it again in case of doubt
452 # run it again in case of doubt
435 # assert not (set(bheadrevs) & set(newheadrevs))
453 # assert not (set(bheadrevs) & set(newheadrevs))
436 bheadset.update(newheadrevs)
454 bheadset.update(newheadrevs)
437
455
438 # This prunes out two kinds of heads - heads that are superseded by
456 # This prunes out two kinds of heads - heads that are superseded by
439 # a head in newheadrevs, and newheadrevs that are not heads because
457 # a head in newheadrevs, and newheadrevs that are not heads because
440 # an existing head is their descendant.
458 # an existing head is their descendant.
441 uncertain = bheadset - topoheads
459 uncertain = bheadset - topoheads
442 if uncertain:
460 if uncertain:
443 floorrev = min(uncertain)
461 floorrev = min(uncertain)
444 ancestors = set(cl.ancestors(newheadrevs, floorrev))
462 ancestors = set(cl.ancestors(newheadrevs, floorrev))
445 bheadset -= ancestors
463 bheadset -= ancestors
446 bheadrevs = sorted(bheadset)
464 bheadrevs = sorted(bheadset)
447 self[branch] = [cl.node(rev) for rev in bheadrevs]
465 self[branch] = [cl.node(rev) for rev in bheadrevs]
448 tiprev = bheadrevs[-1]
466 tiprev = bheadrevs[-1]
449 if tiprev > ntiprev:
467 if tiprev > ntiprev:
450 ntiprev = tiprev
468 ntiprev = tiprev
451
469
452 if ntiprev > self.tiprev:
470 if ntiprev > self.tiprev:
453 self.tiprev = ntiprev
471 self.tiprev = ntiprev
454 self.tipnode = cl.node(ntiprev)
472 self.tipnode = cl.node(ntiprev)
455
473
456 if not self.validfor(repo):
474 if not self.validfor(repo):
457 # cache key are not valid anymore
475 # cache key are not valid anymore
458 self.tipnode = nullid
476 self.tipnode = nullid
459 self.tiprev = nullrev
477 self.tiprev = nullrev
460 for heads in self.iterheads():
478 for heads in self.iterheads():
461 tiprev = max(cl.rev(node) for node in heads)
479 tiprev = max(cl.rev(node) for node in heads)
462 if tiprev > self.tiprev:
480 if tiprev > self.tiprev:
463 self.tipnode = cl.node(tiprev)
481 self.tipnode = cl.node(tiprev)
464 self.tiprev = tiprev
482 self.tiprev = tiprev
465 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
483 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
466
484
467 duration = util.timer() - starttime
485 duration = util.timer() - starttime
468 repo.ui.log(
486 repo.ui.log(
469 b'branchcache',
487 b'branchcache',
470 b'updated %s in %.4f seconds\n',
488 b'updated %s in %.4f seconds\n',
471 _branchcachedesc(repo),
489 _branchcachedesc(repo),
472 duration,
490 duration,
473 )
491 )
474
492
475 self.write(repo)
493 self.write(repo)
476
494
477
495
478 class remotebranchcache(branchcache):
496 class remotebranchcache(branchcache):
479 """Branchmap info for a remote connection, should not write locally"""
497 """Branchmap info for a remote connection, should not write locally"""
480
498
481 def write(self, repo):
499 def write(self, repo):
482 pass
500 pass
483
501
484
502
485 # Revision branch info cache
503 # Revision branch info cache
486
504
487 _rbcversion = b'-v1'
505 _rbcversion = b'-v1'
488 _rbcnames = b'rbc-names' + _rbcversion
506 _rbcnames = b'rbc-names' + _rbcversion
489 _rbcrevs = b'rbc-revs' + _rbcversion
507 _rbcrevs = b'rbc-revs' + _rbcversion
490 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
508 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
491 _rbcrecfmt = b'>4sI'
509 _rbcrecfmt = b'>4sI'
492 _rbcrecsize = calcsize(_rbcrecfmt)
510 _rbcrecsize = calcsize(_rbcrecfmt)
493 _rbcnodelen = 4
511 _rbcnodelen = 4
494 _rbcbranchidxmask = 0x7FFFFFFF
512 _rbcbranchidxmask = 0x7FFFFFFF
495 _rbccloseflag = 0x80000000
513 _rbccloseflag = 0x80000000
496
514
497
515
498 class revbranchcache(object):
516 class revbranchcache(object):
499 """Persistent cache, mapping from revision number to branch name and close.
517 """Persistent cache, mapping from revision number to branch name and close.
500 This is a low level cache, independent of filtering.
518 This is a low level cache, independent of filtering.
501
519
502 Branch names are stored in rbc-names in internal encoding separated by 0.
520 Branch names are stored in rbc-names in internal encoding separated by 0.
503 rbc-names is append-only, and each branch name is only stored once and will
521 rbc-names is append-only, and each branch name is only stored once and will
504 thus have a unique index.
522 thus have a unique index.
505
523
506 The branch info for each revision is stored in rbc-revs as constant size
524 The branch info for each revision is stored in rbc-revs as constant size
507 records. The whole file is read into memory, but it is only 'parsed' on
525 records. The whole file is read into memory, but it is only 'parsed' on
508 demand. The file is usually append-only but will be truncated if repo
526 demand. The file is usually append-only but will be truncated if repo
509 modification is detected.
527 modification is detected.
510 The record for each revision contains the first 4 bytes of the
528 The record for each revision contains the first 4 bytes of the
511 corresponding node hash, and the record is only used if it still matches.
529 corresponding node hash, and the record is only used if it still matches.
512 Even a completely trashed rbc-revs fill thus still give the right result
530 Even a completely trashed rbc-revs fill thus still give the right result
513 while converging towards full recovery ... assuming no incorrectly matching
531 while converging towards full recovery ... assuming no incorrectly matching
514 node hashes.
532 node hashes.
515 The record also contains 4 bytes where 31 bits contains the index of the
533 The record also contains 4 bytes where 31 bits contains the index of the
516 branch and the last bit indicate that it is a branch close commit.
534 branch and the last bit indicate that it is a branch close commit.
517 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
535 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
518 and will grow with it but be 1/8th of its size.
536 and will grow with it but be 1/8th of its size.
519 """
537 """
520
538
521 def __init__(self, repo, readonly=True):
539 def __init__(self, repo, readonly=True):
522 assert repo.filtername is None
540 assert repo.filtername is None
523 self._repo = repo
541 self._repo = repo
524 self._names = [] # branch names in local encoding with static index
542 self._names = [] # branch names in local encoding with static index
525 self._rbcrevs = bytearray()
543 self._rbcrevs = bytearray()
526 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
544 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
527 try:
545 try:
528 bndata = repo.cachevfs.read(_rbcnames)
546 bndata = repo.cachevfs.read(_rbcnames)
529 self._rbcsnameslen = len(bndata) # for verification before writing
547 self._rbcsnameslen = len(bndata) # for verification before writing
530 if bndata:
548 if bndata:
531 self._names = [
549 self._names = [
532 encoding.tolocal(bn) for bn in bndata.split(b'\0')
550 encoding.tolocal(bn) for bn in bndata.split(b'\0')
533 ]
551 ]
534 except (IOError, OSError):
552 except (IOError, OSError):
535 if readonly:
553 if readonly:
536 # don't try to use cache - fall back to the slow path
554 # don't try to use cache - fall back to the slow path
537 self.branchinfo = self._branchinfo
555 self.branchinfo = self._branchinfo
538
556
539 if self._names:
557 if self._names:
540 try:
558 try:
541 data = repo.cachevfs.read(_rbcrevs)
559 data = repo.cachevfs.read(_rbcrevs)
542 self._rbcrevs[:] = data
560 self._rbcrevs[:] = data
543 except (IOError, OSError) as inst:
561 except (IOError, OSError) as inst:
544 repo.ui.debug(
562 repo.ui.debug(
545 b"couldn't read revision branch cache: %s\n"
563 b"couldn't read revision branch cache: %s\n"
546 % stringutil.forcebytestr(inst)
564 % stringutil.forcebytestr(inst)
547 )
565 )
548 # remember number of good records on disk
566 # remember number of good records on disk
549 self._rbcrevslen = min(
567 self._rbcrevslen = min(
550 len(self._rbcrevs) // _rbcrecsize, len(repo.changelog)
568 len(self._rbcrevs) // _rbcrecsize, len(repo.changelog)
551 )
569 )
552 if self._rbcrevslen == 0:
570 if self._rbcrevslen == 0:
553 self._names = []
571 self._names = []
554 self._rbcnamescount = len(self._names) # number of names read at
572 self._rbcnamescount = len(self._names) # number of names read at
555 # _rbcsnameslen
573 # _rbcsnameslen
556
574
557 def _clear(self):
575 def _clear(self):
558 self._rbcsnameslen = 0
576 self._rbcsnameslen = 0
559 del self._names[:]
577 del self._names[:]
560 self._rbcnamescount = 0
578 self._rbcnamescount = 0
561 self._rbcrevslen = len(self._repo.changelog)
579 self._rbcrevslen = len(self._repo.changelog)
562 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
580 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
563 util.clearcachedproperty(self, b'_namesreverse')
581 util.clearcachedproperty(self, b'_namesreverse')
564
582
565 @util.propertycache
583 @util.propertycache
566 def _namesreverse(self):
584 def _namesreverse(self):
567 return dict((b, r) for r, b in enumerate(self._names))
585 return dict((b, r) for r, b in enumerate(self._names))
568
586
569 def branchinfo(self, rev):
587 def branchinfo(self, rev):
570 """Return branch name and close flag for rev, using and updating
588 """Return branch name and close flag for rev, using and updating
571 persistent cache."""
589 persistent cache."""
572 changelog = self._repo.changelog
590 changelog = self._repo.changelog
573 rbcrevidx = rev * _rbcrecsize
591 rbcrevidx = rev * _rbcrecsize
574
592
575 # avoid negative index, changelog.read(nullrev) is fast without cache
593 # avoid negative index, changelog.read(nullrev) is fast without cache
576 if rev == nullrev:
594 if rev == nullrev:
577 return changelog.branchinfo(rev)
595 return changelog.branchinfo(rev)
578
596
579 # if requested rev isn't allocated, grow and cache the rev info
597 # if requested rev isn't allocated, grow and cache the rev info
580 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
598 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
581 return self._branchinfo(rev)
599 return self._branchinfo(rev)
582
600
583 # fast path: extract data from cache, use it if node is matching
601 # fast path: extract data from cache, use it if node is matching
584 reponode = changelog.node(rev)[:_rbcnodelen]
602 reponode = changelog.node(rev)[:_rbcnodelen]
585 cachenode, branchidx = unpack_from(
603 cachenode, branchidx = unpack_from(
586 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx
604 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx
587 )
605 )
588 close = bool(branchidx & _rbccloseflag)
606 close = bool(branchidx & _rbccloseflag)
589 if close:
607 if close:
590 branchidx &= _rbcbranchidxmask
608 branchidx &= _rbcbranchidxmask
591 if cachenode == b'\0\0\0\0':
609 if cachenode == b'\0\0\0\0':
592 pass
610 pass
593 elif cachenode == reponode:
611 elif cachenode == reponode:
594 try:
612 try:
595 return self._names[branchidx], close
613 return self._names[branchidx], close
596 except IndexError:
614 except IndexError:
597 # recover from invalid reference to unknown branch
615 # recover from invalid reference to unknown branch
598 self._repo.ui.debug(
616 self._repo.ui.debug(
599 b"referenced branch names not found"
617 b"referenced branch names not found"
600 b" - rebuilding revision branch cache from scratch\n"
618 b" - rebuilding revision branch cache from scratch\n"
601 )
619 )
602 self._clear()
620 self._clear()
603 else:
621 else:
604 # rev/node map has changed, invalidate the cache from here up
622 # rev/node map has changed, invalidate the cache from here up
605 self._repo.ui.debug(
623 self._repo.ui.debug(
606 b"history modification detected - truncating "
624 b"history modification detected - truncating "
607 b"revision branch cache to revision %d\n" % rev
625 b"revision branch cache to revision %d\n" % rev
608 )
626 )
609 truncate = rbcrevidx + _rbcrecsize
627 truncate = rbcrevidx + _rbcrecsize
610 del self._rbcrevs[truncate:]
628 del self._rbcrevs[truncate:]
611 self._rbcrevslen = min(self._rbcrevslen, truncate)
629 self._rbcrevslen = min(self._rbcrevslen, truncate)
612
630
613 # fall back to slow path and make sure it will be written to disk
631 # fall back to slow path and make sure it will be written to disk
614 return self._branchinfo(rev)
632 return self._branchinfo(rev)
615
633
616 def _branchinfo(self, rev):
634 def _branchinfo(self, rev):
617 """Retrieve branch info from changelog and update _rbcrevs"""
635 """Retrieve branch info from changelog and update _rbcrevs"""
618 changelog = self._repo.changelog
636 changelog = self._repo.changelog
619 b, close = changelog.branchinfo(rev)
637 b, close = changelog.branchinfo(rev)
620 if b in self._namesreverse:
638 if b in self._namesreverse:
621 branchidx = self._namesreverse[b]
639 branchidx = self._namesreverse[b]
622 else:
640 else:
623 branchidx = len(self._names)
641 branchidx = len(self._names)
624 self._names.append(b)
642 self._names.append(b)
625 self._namesreverse[b] = branchidx
643 self._namesreverse[b] = branchidx
626 reponode = changelog.node(rev)
644 reponode = changelog.node(rev)
627 if close:
645 if close:
628 branchidx |= _rbccloseflag
646 branchidx |= _rbccloseflag
629 self._setcachedata(rev, reponode, branchidx)
647 self._setcachedata(rev, reponode, branchidx)
630 return b, close
648 return b, close
631
649
632 def setdata(self, branch, rev, node, close):
650 def setdata(self, branch, rev, node, close):
633 """add new data information to the cache"""
651 """add new data information to the cache"""
634 if branch in self._namesreverse:
652 if branch in self._namesreverse:
635 branchidx = self._namesreverse[branch]
653 branchidx = self._namesreverse[branch]
636 else:
654 else:
637 branchidx = len(self._names)
655 branchidx = len(self._names)
638 self._names.append(branch)
656 self._names.append(branch)
639 self._namesreverse[branch] = branchidx
657 self._namesreverse[branch] = branchidx
640 if close:
658 if close:
641 branchidx |= _rbccloseflag
659 branchidx |= _rbccloseflag
642 self._setcachedata(rev, node, branchidx)
660 self._setcachedata(rev, node, branchidx)
643 # If no cache data were readable (non exists, bad permission, etc)
661 # If no cache data were readable (non exists, bad permission, etc)
644 # the cache was bypassing itself by setting:
662 # the cache was bypassing itself by setting:
645 #
663 #
646 # self.branchinfo = self._branchinfo
664 # self.branchinfo = self._branchinfo
647 #
665 #
648 # Since we now have data in the cache, we need to drop this bypassing.
666 # Since we now have data in the cache, we need to drop this bypassing.
649 if 'branchinfo' in vars(self):
667 if 'branchinfo' in vars(self):
650 del self.branchinfo
668 del self.branchinfo
651
669
652 def _setcachedata(self, rev, node, branchidx):
670 def _setcachedata(self, rev, node, branchidx):
653 """Writes the node's branch data to the in-memory cache data."""
671 """Writes the node's branch data to the in-memory cache data."""
654 if rev == nullrev:
672 if rev == nullrev:
655 return
673 return
656 rbcrevidx = rev * _rbcrecsize
674 rbcrevidx = rev * _rbcrecsize
657 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
675 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
658 self._rbcrevs.extend(
676 self._rbcrevs.extend(
659 b'\0'
677 b'\0'
660 * (len(self._repo.changelog) * _rbcrecsize - len(self._rbcrevs))
678 * (len(self._repo.changelog) * _rbcrecsize - len(self._rbcrevs))
661 )
679 )
662 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
680 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
663 self._rbcrevslen = min(self._rbcrevslen, rev)
681 self._rbcrevslen = min(self._rbcrevslen, rev)
664
682
665 tr = self._repo.currenttransaction()
683 tr = self._repo.currenttransaction()
666 if tr:
684 if tr:
667 tr.addfinalize(b'write-revbranchcache', self.write)
685 tr.addfinalize(b'write-revbranchcache', self.write)
668
686
669 def write(self, tr=None):
687 def write(self, tr=None):
670 """Save branch cache if it is dirty."""
688 """Save branch cache if it is dirty."""
671 repo = self._repo
689 repo = self._repo
672 wlock = None
690 wlock = None
673 step = b''
691 step = b''
674 try:
692 try:
675 # write the new names
693 # write the new names
676 if self._rbcnamescount < len(self._names):
694 if self._rbcnamescount < len(self._names):
677 wlock = repo.wlock(wait=False)
695 wlock = repo.wlock(wait=False)
678 step = b' names'
696 step = b' names'
679 self._writenames(repo)
697 self._writenames(repo)
680
698
681 # write the new revs
699 # write the new revs
682 start = self._rbcrevslen * _rbcrecsize
700 start = self._rbcrevslen * _rbcrecsize
683 if start != len(self._rbcrevs):
701 if start != len(self._rbcrevs):
684 step = b''
702 step = b''
685 if wlock is None:
703 if wlock is None:
686 wlock = repo.wlock(wait=False)
704 wlock = repo.wlock(wait=False)
687 self._writerevs(repo, start)
705 self._writerevs(repo, start)
688
706
689 except (IOError, OSError, error.Abort, error.LockError) as inst:
707 except (IOError, OSError, error.Abort, error.LockError) as inst:
690 repo.ui.debug(
708 repo.ui.debug(
691 b"couldn't write revision branch cache%s: %s\n"
709 b"couldn't write revision branch cache%s: %s\n"
692 % (step, stringutil.forcebytestr(inst))
710 % (step, stringutil.forcebytestr(inst))
693 )
711 )
694 finally:
712 finally:
695 if wlock is not None:
713 if wlock is not None:
696 wlock.release()
714 wlock.release()
697
715
698 def _writenames(self, repo):
716 def _writenames(self, repo):
699 """ write the new branch names to revbranchcache """
717 """ write the new branch names to revbranchcache """
700 if self._rbcnamescount != 0:
718 if self._rbcnamescount != 0:
701 f = repo.cachevfs.open(_rbcnames, b'ab')
719 f = repo.cachevfs.open(_rbcnames, b'ab')
702 if f.tell() == self._rbcsnameslen:
720 if f.tell() == self._rbcsnameslen:
703 f.write(b'\0')
721 f.write(b'\0')
704 else:
722 else:
705 f.close()
723 f.close()
706 repo.ui.debug(b"%s changed - rewriting it\n" % _rbcnames)
724 repo.ui.debug(b"%s changed - rewriting it\n" % _rbcnames)
707 self._rbcnamescount = 0
725 self._rbcnamescount = 0
708 self._rbcrevslen = 0
726 self._rbcrevslen = 0
709 if self._rbcnamescount == 0:
727 if self._rbcnamescount == 0:
710 # before rewriting names, make sure references are removed
728 # before rewriting names, make sure references are removed
711 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
729 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
712 f = repo.cachevfs.open(_rbcnames, b'wb')
730 f = repo.cachevfs.open(_rbcnames, b'wb')
713 f.write(
731 f.write(
714 b'\0'.join(
732 b'\0'.join(
715 encoding.fromlocal(b)
733 encoding.fromlocal(b)
716 for b in self._names[self._rbcnamescount :]
734 for b in self._names[self._rbcnamescount :]
717 )
735 )
718 )
736 )
719 self._rbcsnameslen = f.tell()
737 self._rbcsnameslen = f.tell()
720 f.close()
738 f.close()
721 self._rbcnamescount = len(self._names)
739 self._rbcnamescount = len(self._names)
722
740
723 def _writerevs(self, repo, start):
741 def _writerevs(self, repo, start):
724 """ write the new revs to revbranchcache """
742 """ write the new revs to revbranchcache """
725 revs = min(len(repo.changelog), len(self._rbcrevs) // _rbcrecsize)
743 revs = min(len(repo.changelog), len(self._rbcrevs) // _rbcrecsize)
726 with repo.cachevfs.open(_rbcrevs, b'ab') as f:
744 with repo.cachevfs.open(_rbcrevs, b'ab') as f:
727 if f.tell() != start:
745 if f.tell() != start:
728 repo.ui.debug(
746 repo.ui.debug(
729 b"truncating cache/%s to %d\n" % (_rbcrevs, start)
747 b"truncating cache/%s to %d\n" % (_rbcrevs, start)
730 )
748 )
731 f.seek(start)
749 f.seek(start)
732 if f.tell() != start:
750 if f.tell() != start:
733 start = 0
751 start = 0
734 f.seek(start)
752 f.seek(start)
735 f.truncate()
753 f.truncate()
736 end = revs * _rbcrecsize
754 end = revs * _rbcrecsize
737 f.write(self._rbcrevs[start:end])
755 f.write(self._rbcrevs[start:end])
738 self._rbcrevslen = revs
756 self._rbcrevslen = revs
General Comments 0
You need to be logged in to leave comments. Login now