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