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