##// END OF EJS Templates
branchmap-v3: detect invalid headerline and raise error...
marmoute -
r52865:dd3ccda3 default
parent child Browse files
Show More
@@ -1,1083 +1,1087
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 annotations
8 from __future__ import annotations
9
9
10 from .node import (
10 from .node import (
11 bin,
11 bin,
12 hex,
12 hex,
13 nullrev,
13 nullrev,
14 )
14 )
15
15
16 from typing import (
16 from typing import (
17 Any,
17 Any,
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 cast,
27 cast,
28 )
28 )
29
29
30 from . import (
30 from . import (
31 encoding,
31 encoding,
32 error,
32 error,
33 obsolete,
33 obsolete,
34 scmutil,
34 scmutil,
35 util,
35 util,
36 )
36 )
37
37
38 from .utils import (
38 from .utils import (
39 repoviewutil,
39 repoviewutil,
40 stringutil,
40 stringutil,
41 )
41 )
42
42
43 if TYPE_CHECKING:
43 if TYPE_CHECKING:
44 from . import localrepo
44 from . import localrepo
45
45
46 assert [localrepo]
46 assert [localrepo]
47
47
48 subsettable = repoviewutil.subsettable
48 subsettable = repoviewutil.subsettable
49
49
50
50
51 class BranchMapCache:
51 class BranchMapCache:
52 """mapping of filtered views of repo with their branchcache"""
52 """mapping of filtered views of repo with their branchcache"""
53
53
54 def __init__(self):
54 def __init__(self):
55 self._per_filter = {}
55 self._per_filter = {}
56
56
57 def __getitem__(self, repo):
57 def __getitem__(self, repo):
58 self.updatecache(repo)
58 self.updatecache(repo)
59 bcache = self._per_filter[repo.filtername]
59 bcache = self._per_filter[repo.filtername]
60 bcache._ensure_populated(repo)
60 bcache._ensure_populated(repo)
61 assert bcache._filtername == repo.filtername, (
61 assert bcache._filtername == repo.filtername, (
62 bcache._filtername,
62 bcache._filtername,
63 repo.filtername,
63 repo.filtername,
64 )
64 )
65 return bcache
65 return bcache
66
66
67 def update_disk(self, repo, detect_pure_topo=False):
67 def update_disk(self, repo, detect_pure_topo=False):
68 """ensure and up-to-date cache is (or will be) written on disk
68 """ensure and up-to-date cache is (or will be) written on disk
69
69
70 The cache for this repository view is updated if needed and written on
70 The cache for this repository view is updated if needed and written on
71 disk.
71 disk.
72
72
73 If a transaction is in progress, the writing is schedule to transaction
73 If a transaction is in progress, the writing is schedule to transaction
74 close. See the `BranchMapCache.write_dirty` method.
74 close. See the `BranchMapCache.write_dirty` method.
75
75
76 This method exist independently of __getitem__ as it is sometime useful
76 This method exist independently of __getitem__ as it is sometime useful
77 to signal that we have no intend to use the data in memory yet.
77 to signal that we have no intend to use the data in memory yet.
78 """
78 """
79 self.updatecache(repo)
79 self.updatecache(repo)
80 bcache = self._per_filter[repo.filtername]
80 bcache = self._per_filter[repo.filtername]
81 assert bcache._filtername == repo.filtername, (
81 assert bcache._filtername == repo.filtername, (
82 bcache._filtername,
82 bcache._filtername,
83 repo.filtername,
83 repo.filtername,
84 )
84 )
85 if detect_pure_topo:
85 if detect_pure_topo:
86 bcache._detect_pure_topo(repo)
86 bcache._detect_pure_topo(repo)
87 tr = repo.currenttransaction()
87 tr = repo.currenttransaction()
88 if getattr(tr, 'finalized', True):
88 if getattr(tr, 'finalized', True):
89 bcache.sync_disk(repo)
89 bcache.sync_disk(repo)
90
90
91 def updatecache(self, repo):
91 def updatecache(self, repo):
92 """Update the cache for the given filtered view on a repository"""
92 """Update the cache for the given filtered view on a repository"""
93 # This can trigger updates for the caches for subsets of the filtered
93 # This can trigger updates for the caches for subsets of the filtered
94 # view, e.g. when there is no cache for this filtered view or the cache
94 # view, e.g. when there is no cache for this filtered view or the cache
95 # is stale.
95 # is stale.
96
96
97 cl = repo.changelog
97 cl = repo.changelog
98 filtername = repo.filtername
98 filtername = repo.filtername
99 bcache = self._per_filter.get(filtername)
99 bcache = self._per_filter.get(filtername)
100 if bcache is None or not bcache.validfor(repo):
100 if bcache is None or not bcache.validfor(repo):
101 # cache object missing or cache object stale? Read from disk
101 # cache object missing or cache object stale? Read from disk
102 bcache = branch_cache_from_file(repo)
102 bcache = branch_cache_from_file(repo)
103
103
104 revs = []
104 revs = []
105 if bcache is None:
105 if bcache is None:
106 # no (fresh) cache available anymore, perhaps we can re-use
106 # no (fresh) cache available anymore, perhaps we can re-use
107 # the cache for a subset, then extend that to add info on missing
107 # the cache for a subset, then extend that to add info on missing
108 # revisions.
108 # revisions.
109 subsetname = subsettable.get(filtername)
109 subsetname = subsettable.get(filtername)
110 if subsetname is not None:
110 if subsetname is not None:
111 subset = repo.filtered(subsetname)
111 subset = repo.filtered(subsetname)
112 self.updatecache(subset)
112 self.updatecache(subset)
113 bcache = self._per_filter[subset.filtername].inherit_for(repo)
113 bcache = self._per_filter[subset.filtername].inherit_for(repo)
114 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
114 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
115 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
115 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
116 else:
116 else:
117 # nothing to fall back on, start empty.
117 # nothing to fall back on, start empty.
118 bcache = new_branch_cache(repo)
118 bcache = new_branch_cache(repo)
119
119
120 revs.extend(cl.revs(start=bcache.tiprev + 1))
120 revs.extend(cl.revs(start=bcache.tiprev + 1))
121 if revs:
121 if revs:
122 bcache.update(repo, revs)
122 bcache.update(repo, revs)
123
123
124 assert bcache.validfor(repo), filtername
124 assert bcache.validfor(repo), filtername
125 self._per_filter[repo.filtername] = bcache
125 self._per_filter[repo.filtername] = bcache
126
126
127 def replace(self, repo, remotebranchmap):
127 def replace(self, repo, remotebranchmap):
128 """Replace the branchmap cache for a repo with a branch mapping.
128 """Replace the branchmap cache for a repo with a branch mapping.
129
129
130 This is likely only called during clone with a branch map from a
130 This is likely only called during clone with a branch map from a
131 remote.
131 remote.
132
132
133 """
133 """
134 cl = repo.changelog
134 cl = repo.changelog
135 clrev = cl.rev
135 clrev = cl.rev
136 clbranchinfo = cl.branchinfo
136 clbranchinfo = cl.branchinfo
137 rbheads = []
137 rbheads = []
138 closed = set()
138 closed = set()
139 for bheads in remotebranchmap.values():
139 for bheads in remotebranchmap.values():
140 rbheads += bheads
140 rbheads += bheads
141 for h in bheads:
141 for h in bheads:
142 r = clrev(h)
142 r = clrev(h)
143 b, c = clbranchinfo(r)
143 b, c = clbranchinfo(r)
144 if c:
144 if c:
145 closed.add(h)
145 closed.add(h)
146
146
147 if rbheads:
147 if rbheads:
148 rtiprev = max((int(clrev(node)) for node in rbheads))
148 rtiprev = max((int(clrev(node)) for node in rbheads))
149 cache = new_branch_cache(
149 cache = new_branch_cache(
150 repo,
150 repo,
151 remotebranchmap,
151 remotebranchmap,
152 repo[rtiprev].node(),
152 repo[rtiprev].node(),
153 rtiprev,
153 rtiprev,
154 closednodes=closed,
154 closednodes=closed,
155 )
155 )
156
156
157 # Try to stick it as low as possible
157 # Try to stick it as low as possible
158 # filter above served are unlikely to be fetch from a clone
158 # filter above served are unlikely to be fetch from a clone
159 for candidate in (b'base', b'immutable', b'served'):
159 for candidate in (b'base', b'immutable', b'served'):
160 rview = repo.filtered(candidate)
160 rview = repo.filtered(candidate)
161 if cache.validfor(rview):
161 if cache.validfor(rview):
162 cache._filtername = candidate
162 cache._filtername = candidate
163 self._per_filter[candidate] = cache
163 self._per_filter[candidate] = cache
164 cache._state = STATE_DIRTY
164 cache._state = STATE_DIRTY
165 cache.write(rview)
165 cache.write(rview)
166 return
166 return
167
167
168 def clear(self):
168 def clear(self):
169 self._per_filter.clear()
169 self._per_filter.clear()
170
170
171 def write_dirty(self, repo):
171 def write_dirty(self, repo):
172 unfi = repo.unfiltered()
172 unfi = repo.unfiltered()
173 for filtername in repoviewutil.get_ordered_subset():
173 for filtername in repoviewutil.get_ordered_subset():
174 cache = self._per_filter.get(filtername)
174 cache = self._per_filter.get(filtername)
175 if cache is None:
175 if cache is None:
176 continue
176 continue
177 if filtername is None:
177 if filtername is None:
178 repo = unfi
178 repo = unfi
179 else:
179 else:
180 repo = unfi.filtered(filtername)
180 repo = unfi.filtered(filtername)
181 cache.sync_disk(repo)
181 cache.sync_disk(repo)
182
182
183
183
184 def _unknownnode(node):
184 def _unknownnode(node):
185 """raises ValueError when branchcache found a node which does not exists"""
185 """raises ValueError when branchcache found a node which does not exists"""
186 raise ValueError('node %s does not exist' % node.hex())
186 raise ValueError('node %s does not exist' % node.hex())
187
187
188
188
189 def _branchcachedesc(repo):
189 def _branchcachedesc(repo):
190 if repo.filtername is not None:
190 if repo.filtername is not None:
191 return b'branch cache (%s)' % repo.filtername
191 return b'branch cache (%s)' % repo.filtername
192 else:
192 else:
193 return b'branch cache'
193 return b'branch cache'
194
194
195
195
196 class _BaseBranchCache:
196 class _BaseBranchCache:
197 """A dict like object that hold branches heads cache.
197 """A dict like object that hold branches heads cache.
198
198
199 This cache is used to avoid costly computations to determine all the
199 This cache is used to avoid costly computations to determine all the
200 branch heads of a repo.
200 branch heads of a repo.
201 """
201 """
202
202
203 def __init__(
203 def __init__(
204 self,
204 self,
205 repo: "localrepo.localrepository",
205 repo: "localrepo.localrepository",
206 entries: Union[
206 entries: Union[
207 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
207 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
208 ] = (),
208 ] = (),
209 closed_nodes: Optional[Set[bytes]] = None,
209 closed_nodes: Optional[Set[bytes]] = None,
210 ) -> None:
210 ) -> None:
211 """hasnode is a function which can be used to verify whether changelog
211 """hasnode is a function which can be used to verify whether changelog
212 has a given node or not. If it's not provided, we assume that every node
212 has a given node or not. If it's not provided, we assume that every node
213 we have exists in changelog"""
213 we have exists in changelog"""
214 # closednodes is a set of nodes that close their branch. If the branch
214 # closednodes is a set of nodes that close their branch. If the branch
215 # cache has been updated, it may contain nodes that are no longer
215 # cache has been updated, it may contain nodes that are no longer
216 # heads.
216 # heads.
217 if closed_nodes is None:
217 if closed_nodes is None:
218 closed_nodes = set()
218 closed_nodes = set()
219 self._closednodes = set(closed_nodes)
219 self._closednodes = set(closed_nodes)
220 self._entries = dict(entries)
220 self._entries = dict(entries)
221
221
222 def __iter__(self):
222 def __iter__(self):
223 return iter(self._entries)
223 return iter(self._entries)
224
224
225 def __setitem__(self, key, value):
225 def __setitem__(self, key, value):
226 self._entries[key] = value
226 self._entries[key] = value
227
227
228 def __getitem__(self, key):
228 def __getitem__(self, key):
229 return self._entries[key]
229 return self._entries[key]
230
230
231 def __contains__(self, key):
231 def __contains__(self, key):
232 return key in self._entries
232 return key in self._entries
233
233
234 def iteritems(self):
234 def iteritems(self):
235 return self._entries.items()
235 return self._entries.items()
236
236
237 items = iteritems
237 items = iteritems
238
238
239 def hasbranch(self, label):
239 def hasbranch(self, label):
240 """checks whether a branch of this name exists or not"""
240 """checks whether a branch of this name exists or not"""
241 return label in self._entries
241 return label in self._entries
242
242
243 def _branchtip(self, heads):
243 def _branchtip(self, heads):
244 """Return tuple with last open head in heads and false,
244 """Return tuple with last open head in heads and false,
245 otherwise return last closed head and true."""
245 otherwise return last closed head and true."""
246 tip = heads[-1]
246 tip = heads[-1]
247 closed = True
247 closed = True
248 for h in reversed(heads):
248 for h in reversed(heads):
249 if h not in self._closednodes:
249 if h not in self._closednodes:
250 tip = h
250 tip = h
251 closed = False
251 closed = False
252 break
252 break
253 return tip, closed
253 return tip, closed
254
254
255 def branchtip(self, branch):
255 def branchtip(self, branch):
256 """Return the tipmost open head on branch head, otherwise return the
256 """Return the tipmost open head on branch head, otherwise return the
257 tipmost closed head on branch.
257 tipmost closed head on branch.
258 Raise KeyError for unknown branch."""
258 Raise KeyError for unknown branch."""
259 return self._branchtip(self[branch])[0]
259 return self._branchtip(self[branch])[0]
260
260
261 def iteropen(self, nodes):
261 def iteropen(self, nodes):
262 return (n for n in nodes if n not in self._closednodes)
262 return (n for n in nodes if n not in self._closednodes)
263
263
264 def branchheads(self, branch, closed=False):
264 def branchheads(self, branch, closed=False):
265 heads = self._entries[branch]
265 heads = self._entries[branch]
266 if not closed:
266 if not closed:
267 heads = list(self.iteropen(heads))
267 heads = list(self.iteropen(heads))
268 return heads
268 return heads
269
269
270 def iterbranches(self):
270 def iterbranches(self):
271 for bn, heads in self.items():
271 for bn, heads in self.items():
272 yield (bn, heads) + self._branchtip(heads)
272 yield (bn, heads) + self._branchtip(heads)
273
273
274 def iterheads(self):
274 def iterheads(self):
275 """returns all the heads"""
275 """returns all the heads"""
276 return self._entries.values()
276 return self._entries.values()
277
277
278 def update(self, repo, revgen):
278 def update(self, repo, revgen):
279 """Given a branchhead cache, self, that may have extra nodes or be
279 """Given a branchhead cache, self, that may have extra nodes or be
280 missing heads, and a generator of nodes that are strictly a superset of
280 missing heads, and a generator of nodes that are strictly a superset of
281 heads missing, this function updates self to be correct.
281 heads missing, this function updates self to be correct.
282 """
282 """
283 starttime = util.timer()
283 starttime = util.timer()
284 cl = repo.changelog
284 cl = repo.changelog
285 # Faster than using ctx.obsolete()
285 # Faster than using ctx.obsolete()
286 obsrevs = obsolete.getrevs(repo, b'obsolete')
286 obsrevs = obsolete.getrevs(repo, b'obsolete')
287 # collect new branch entries
287 # collect new branch entries
288 newbranches = {}
288 newbranches = {}
289 new_closed = set()
289 new_closed = set()
290 obs_ignored = set()
290 obs_ignored = set()
291 getbranchinfo = repo.revbranchcache().branchinfo
291 getbranchinfo = repo.revbranchcache().branchinfo
292 max_rev = -1
292 max_rev = -1
293 for r in revgen:
293 for r in revgen:
294 max_rev = max(max_rev, r)
294 max_rev = max(max_rev, r)
295 if r in obsrevs:
295 if r in obsrevs:
296 # We ignore obsolete changesets as they shouldn't be
296 # We ignore obsolete changesets as they shouldn't be
297 # considered heads.
297 # considered heads.
298 obs_ignored.add(r)
298 obs_ignored.add(r)
299 continue
299 continue
300 branch, closesbranch = getbranchinfo(r)
300 branch, closesbranch = getbranchinfo(r)
301 newbranches.setdefault(branch, []).append(r)
301 newbranches.setdefault(branch, []).append(r)
302 if closesbranch:
302 if closesbranch:
303 new_closed.add(r)
303 new_closed.add(r)
304 if max_rev < 0:
304 if max_rev < 0:
305 msg = "running branchcache.update without revision to update"
305 msg = "running branchcache.update without revision to update"
306 raise error.ProgrammingError(msg)
306 raise error.ProgrammingError(msg)
307
307
308 self._process_new(
308 self._process_new(
309 repo,
309 repo,
310 newbranches,
310 newbranches,
311 new_closed,
311 new_closed,
312 obs_ignored,
312 obs_ignored,
313 max_rev,
313 max_rev,
314 )
314 )
315
315
316 self._closednodes.update(cl.node(rev) for rev in new_closed)
316 self._closednodes.update(cl.node(rev) for rev in new_closed)
317
317
318 duration = util.timer() - starttime
318 duration = util.timer() - starttime
319 repo.ui.log(
319 repo.ui.log(
320 b'branchcache',
320 b'branchcache',
321 b'updated %s in %.4f seconds\n',
321 b'updated %s in %.4f seconds\n',
322 _branchcachedesc(repo),
322 _branchcachedesc(repo),
323 duration,
323 duration,
324 )
324 )
325 return max_rev
325 return max_rev
326
326
327 def _process_new(
327 def _process_new(
328 self,
328 self,
329 repo,
329 repo,
330 newbranches,
330 newbranches,
331 new_closed,
331 new_closed,
332 obs_ignored,
332 obs_ignored,
333 max_rev,
333 max_rev,
334 ):
334 ):
335 """update the branchmap from a set of new information"""
335 """update the branchmap from a set of new information"""
336 # Delay fetching the topological heads until they are needed.
336 # Delay fetching the topological heads until they are needed.
337 # A repository without non-continous branches can skip this part.
337 # A repository without non-continous branches can skip this part.
338 topoheads = None
338 topoheads = None
339
339
340 cl = repo.changelog
340 cl = repo.changelog
341 getbranchinfo = repo.revbranchcache().branchinfo
341 getbranchinfo = repo.revbranchcache().branchinfo
342 # Faster than using ctx.obsolete()
342 # Faster than using ctx.obsolete()
343 obsrevs = obsolete.getrevs(repo, b'obsolete')
343 obsrevs = obsolete.getrevs(repo, b'obsolete')
344
344
345 # If a changeset is visible, its parents must be visible too, so
345 # If a changeset is visible, its parents must be visible too, so
346 # use the faster unfiltered parent accessor.
346 # use the faster unfiltered parent accessor.
347 parentrevs = cl._uncheckedparentrevs
347 parentrevs = cl._uncheckedparentrevs
348
348
349 for branch, newheadrevs in newbranches.items():
349 for branch, newheadrevs in newbranches.items():
350 # For every branch, compute the new branchheads.
350 # For every branch, compute the new branchheads.
351 # A branchhead is a revision such that no descendant is on
351 # A branchhead is a revision such that no descendant is on
352 # the same branch.
352 # the same branch.
353 #
353 #
354 # The branchheads are computed iteratively in revision order.
354 # The branchheads are computed iteratively in revision order.
355 # This ensures topological order, i.e. parents are processed
355 # This ensures topological order, i.e. parents are processed
356 # before their children. Ancestors are inclusive here, i.e.
356 # before their children. Ancestors are inclusive here, i.e.
357 # any revision is an ancestor of itself.
357 # any revision is an ancestor of itself.
358 #
358 #
359 # Core observations:
359 # Core observations:
360 # - The current revision is always a branchhead for the
360 # - The current revision is always a branchhead for the
361 # repository up to that point.
361 # repository up to that point.
362 # - It is the first revision of the branch if and only if
362 # - It is the first revision of the branch if and only if
363 # there was no branchhead before. In that case, it is the
363 # there was no branchhead before. In that case, it is the
364 # only branchhead as there are no possible ancestors on
364 # only branchhead as there are no possible ancestors on
365 # the same branch.
365 # the same branch.
366 # - If a parent is on the same branch, a branchhead can
366 # - If a parent is on the same branch, a branchhead can
367 # only be an ancestor of that parent, if it is parent
367 # only be an ancestor of that parent, if it is parent
368 # itself. Otherwise it would have been removed as ancestor
368 # itself. Otherwise it would have been removed as ancestor
369 # of that parent before.
369 # of that parent before.
370 # - Therefore, if all parents are on the same branch, they
370 # - Therefore, if all parents are on the same branch, they
371 # can just be removed from the branchhead set.
371 # can just be removed from the branchhead set.
372 # - If one parent is on the same branch and the other is not
372 # - If one parent is on the same branch and the other is not
373 # and there was exactly one branchhead known, the existing
373 # and there was exactly one branchhead known, the existing
374 # branchhead can only be an ancestor if it is the parent.
374 # branchhead can only be an ancestor if it is the parent.
375 # Otherwise it would have been removed as ancestor of
375 # Otherwise it would have been removed as ancestor of
376 # the parent before. The other parent therefore can't have
376 # the parent before. The other parent therefore can't have
377 # a branchhead as ancestor.
377 # a branchhead as ancestor.
378 # - In all other cases, the parents on different branches
378 # - In all other cases, the parents on different branches
379 # could have a branchhead as ancestor. Those parents are
379 # could have a branchhead as ancestor. Those parents are
380 # kept in the "uncertain" set. If all branchheads are also
380 # kept in the "uncertain" set. If all branchheads are also
381 # topological heads, they can't have descendants and further
381 # topological heads, they can't have descendants and further
382 # checks can be skipped. Otherwise, the ancestors of the
382 # checks can be skipped. Otherwise, the ancestors of the
383 # "uncertain" set are removed from branchheads.
383 # "uncertain" set are removed from branchheads.
384 # This computation is heavy and avoided if at all possible.
384 # This computation is heavy and avoided if at all possible.
385 bheads = self._entries.get(branch, [])
385 bheads = self._entries.get(branch, [])
386 bheadset = {cl.rev(node) for node in bheads}
386 bheadset = {cl.rev(node) for node in bheads}
387 uncertain = set()
387 uncertain = set()
388 for newrev in sorted(newheadrevs):
388 for newrev in sorted(newheadrevs):
389 if not bheadset:
389 if not bheadset:
390 bheadset.add(newrev)
390 bheadset.add(newrev)
391 continue
391 continue
392
392
393 parents = [p for p in parentrevs(newrev) if p != nullrev]
393 parents = [p for p in parentrevs(newrev) if p != nullrev]
394 samebranch = set()
394 samebranch = set()
395 otherbranch = set()
395 otherbranch = set()
396 obsparents = set()
396 obsparents = set()
397 for p in parents:
397 for p in parents:
398 if p in obsrevs:
398 if p in obsrevs:
399 # We ignored this obsolete changeset earlier, but now
399 # We ignored this obsolete changeset earlier, but now
400 # that it has non-ignored children, we need to make
400 # that it has non-ignored children, we need to make
401 # sure their ancestors are not considered heads. To
401 # sure their ancestors are not considered heads. To
402 # achieve that, we will simply treat this obsolete
402 # achieve that, we will simply treat this obsolete
403 # changeset as a parent from other branch.
403 # changeset as a parent from other branch.
404 obsparents.add(p)
404 obsparents.add(p)
405 elif p in bheadset or getbranchinfo(p)[0] == branch:
405 elif p in bheadset or getbranchinfo(p)[0] == branch:
406 samebranch.add(p)
406 samebranch.add(p)
407 else:
407 else:
408 otherbranch.add(p)
408 otherbranch.add(p)
409 if not (len(bheadset) == len(samebranch) == 1):
409 if not (len(bheadset) == len(samebranch) == 1):
410 uncertain.update(otherbranch)
410 uncertain.update(otherbranch)
411 uncertain.update(obsparents)
411 uncertain.update(obsparents)
412 bheadset.difference_update(samebranch)
412 bheadset.difference_update(samebranch)
413 bheadset.add(newrev)
413 bheadset.add(newrev)
414
414
415 if uncertain:
415 if uncertain:
416 if topoheads is None:
416 if topoheads is None:
417 topoheads = set(cl.headrevs())
417 topoheads = set(cl.headrevs())
418 if bheadset - topoheads:
418 if bheadset - topoheads:
419 floorrev = min(bheadset)
419 floorrev = min(bheadset)
420 if floorrev <= max(uncertain):
420 if floorrev <= max(uncertain):
421 ancestors = set(cl.ancestors(uncertain, floorrev))
421 ancestors = set(cl.ancestors(uncertain, floorrev))
422 bheadset -= ancestors
422 bheadset -= ancestors
423 if bheadset:
423 if bheadset:
424 self[branch] = [cl.node(rev) for rev in sorted(bheadset)]
424 self[branch] = [cl.node(rev) for rev in sorted(bheadset)]
425
425
426
426
427 STATE_CLEAN = 1
427 STATE_CLEAN = 1
428 STATE_INHERITED = 2
428 STATE_INHERITED = 2
429 STATE_DIRTY = 3
429 STATE_DIRTY = 3
430
430
431
431
432 class _LocalBranchCache(_BaseBranchCache):
432 class _LocalBranchCache(_BaseBranchCache):
433 """base class of branch-map info for a local repo or repoview"""
433 """base class of branch-map info for a local repo or repoview"""
434
434
435 _base_filename = None
435 _base_filename = None
436 _default_key_hashes: Tuple[bytes] = cast(Tuple[bytes], ())
436 _default_key_hashes: Tuple[bytes] = cast(Tuple[bytes], ())
437
437
438 def __init__(
438 def __init__(
439 self,
439 self,
440 repo: "localrepo.localrepository",
440 repo: "localrepo.localrepository",
441 entries: Union[
441 entries: Union[
442 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
442 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
443 ] = (),
443 ] = (),
444 tipnode: Optional[bytes] = None,
444 tipnode: Optional[bytes] = None,
445 tiprev: Optional[int] = nullrev,
445 tiprev: Optional[int] = nullrev,
446 key_hashes: Optional[Tuple[bytes]] = None,
446 key_hashes: Optional[Tuple[bytes]] = None,
447 closednodes: Optional[Set[bytes]] = None,
447 closednodes: Optional[Set[bytes]] = None,
448 hasnode: Optional[Callable[[bytes], bool]] = None,
448 hasnode: Optional[Callable[[bytes], bool]] = None,
449 verify_node: bool = False,
449 verify_node: bool = False,
450 inherited: bool = False,
450 inherited: bool = False,
451 ) -> None:
451 ) -> None:
452 """hasnode is a function which can be used to verify whether changelog
452 """hasnode is a function which can be used to verify whether changelog
453 has a given node or not. If it's not provided, we assume that every node
453 has a given node or not. If it's not provided, we assume that every node
454 we have exists in changelog"""
454 we have exists in changelog"""
455 self._filtername = repo.filtername
455 self._filtername = repo.filtername
456 if tipnode is None:
456 if tipnode is None:
457 self.tipnode = repo.nullid
457 self.tipnode = repo.nullid
458 else:
458 else:
459 self.tipnode = tipnode
459 self.tipnode = tipnode
460 self.tiprev = tiprev
460 self.tiprev = tiprev
461 if key_hashes is None:
461 if key_hashes is None:
462 self.key_hashes = self._default_key_hashes
462 self.key_hashes = self._default_key_hashes
463 else:
463 else:
464 self.key_hashes = key_hashes
464 self.key_hashes = key_hashes
465 self._state = STATE_CLEAN
465 self._state = STATE_CLEAN
466 if inherited:
466 if inherited:
467 self._state = STATE_INHERITED
467 self._state = STATE_INHERITED
468
468
469 super().__init__(repo=repo, entries=entries, closed_nodes=closednodes)
469 super().__init__(repo=repo, entries=entries, closed_nodes=closednodes)
470 # closednodes is a set of nodes that close their branch. If the branch
470 # closednodes is a set of nodes that close their branch. If the branch
471 # cache has been updated, it may contain nodes that are no longer
471 # cache has been updated, it may contain nodes that are no longer
472 # heads.
472 # heads.
473
473
474 # Do we need to verify branch at all ?
474 # Do we need to verify branch at all ?
475 self._verify_node = verify_node
475 self._verify_node = verify_node
476 # branches for which nodes are verified
476 # branches for which nodes are verified
477 self._verifiedbranches = set()
477 self._verifiedbranches = set()
478 self._hasnode = None
478 self._hasnode = None
479 if self._verify_node:
479 if self._verify_node:
480 self._hasnode = repo.changelog.hasnode
480 self._hasnode = repo.changelog.hasnode
481
481
482 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
482 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
483 raise NotImplementedError
483 raise NotImplementedError
484
484
485 def _ensure_populated(self, repo):
485 def _ensure_populated(self, repo):
486 """make sure any lazily loaded values are fully populated"""
486 """make sure any lazily loaded values are fully populated"""
487
487
488 def _detect_pure_topo(self, repo) -> None:
488 def _detect_pure_topo(self, repo) -> None:
489 pass
489 pass
490
490
491 def validfor(self, repo):
491 def validfor(self, repo):
492 """check that cache contents are valid for (a subset of) this repo
492 """check that cache contents are valid for (a subset of) this repo
493
493
494 - False when the order of changesets changed or if we detect a strip.
494 - False when the order of changesets changed or if we detect a strip.
495 - True when cache is up-to-date for the current repo or its subset."""
495 - True when cache is up-to-date for the current repo or its subset."""
496 try:
496 try:
497 node = repo.changelog.node(self.tiprev)
497 node = repo.changelog.node(self.tiprev)
498 except IndexError:
498 except IndexError:
499 # changesets were stripped and now we don't even have enough to
499 # changesets were stripped and now we don't even have enough to
500 # find tiprev
500 # find tiprev
501 return False
501 return False
502 if self.tipnode != node:
502 if self.tipnode != node:
503 # tiprev doesn't correspond to tipnode: repo was stripped, or this
503 # tiprev doesn't correspond to tipnode: repo was stripped, or this
504 # repo has a different order of changesets
504 # repo has a different order of changesets
505 return False
505 return False
506 repo_key_hashes = self._compute_key_hashes(repo)
506 repo_key_hashes = self._compute_key_hashes(repo)
507 # hashes don't match if this repo view has a different set of filtered
507 # hashes don't match if this repo view has a different set of filtered
508 # revisions (e.g. due to phase changes) or obsolete revisions (e.g.
508 # revisions (e.g. due to phase changes) or obsolete revisions (e.g.
509 # history was rewritten)
509 # history was rewritten)
510 return self.key_hashes == repo_key_hashes
510 return self.key_hashes == repo_key_hashes
511
511
512 @classmethod
512 @classmethod
513 def fromfile(cls, repo):
513 def fromfile(cls, repo):
514 f = None
514 f = None
515 try:
515 try:
516 f = repo.cachevfs(cls._filename(repo))
516 f = repo.cachevfs(cls._filename(repo))
517 lineiter = iter(f)
517 lineiter = iter(f)
518 init_kwargs = cls._load_header(repo, lineiter)
518 init_kwargs = cls._load_header(repo, lineiter)
519 bcache = cls(
519 bcache = cls(
520 repo,
520 repo,
521 verify_node=True,
521 verify_node=True,
522 **init_kwargs,
522 **init_kwargs,
523 )
523 )
524 if not bcache.validfor(repo):
524 if not bcache.validfor(repo):
525 # invalidate the cache
525 # invalidate the cache
526 raise ValueError('tip differs')
526 raise ValueError('tip differs')
527 bcache._load_heads(repo, lineiter)
527 bcache._load_heads(repo, lineiter)
528 except (IOError, OSError):
528 except (IOError, OSError):
529 return None
529 return None
530
530
531 except Exception as inst:
531 except Exception as inst:
532 if repo.ui.debugflag:
532 if repo.ui.debugflag:
533 msg = b'invalid %s: %s\n'
533 msg = b'invalid %s: %s\n'
534 msg %= (
534 msg %= (
535 _branchcachedesc(repo),
535 _branchcachedesc(repo),
536 stringutil.forcebytestr(inst),
536 stringutil.forcebytestr(inst),
537 )
537 )
538 repo.ui.debug(msg)
538 repo.ui.debug(msg)
539 bcache = None
539 bcache = None
540
540
541 finally:
541 finally:
542 if f:
542 if f:
543 f.close()
543 f.close()
544
544
545 return bcache
545 return bcache
546
546
547 @classmethod
547 @classmethod
548 def _load_header(cls, repo, lineiter) -> "dict[str, Any]":
548 def _load_header(cls, repo, lineiter) -> "dict[str, Any]":
549 raise NotImplementedError
549 raise NotImplementedError
550
550
551 def _load_heads(self, repo, lineiter):
551 def _load_heads(self, repo, lineiter):
552 """fully loads the branchcache by reading from the file using the line
552 """fully loads the branchcache by reading from the file using the line
553 iterator passed"""
553 iterator passed"""
554 for line in lineiter:
554 for line in lineiter:
555 line = line.rstrip(b'\n')
555 line = line.rstrip(b'\n')
556 if not line:
556 if not line:
557 continue
557 continue
558 node, state, label = line.split(b" ", 2)
558 node, state, label = line.split(b" ", 2)
559 if state not in b'oc':
559 if state not in b'oc':
560 raise ValueError('invalid branch state')
560 raise ValueError('invalid branch state')
561 label = encoding.tolocal(label.strip())
561 label = encoding.tolocal(label.strip())
562 node = bin(node)
562 node = bin(node)
563 self._entries.setdefault(label, []).append(node)
563 self._entries.setdefault(label, []).append(node)
564 if state == b'c':
564 if state == b'c':
565 self._closednodes.add(node)
565 self._closednodes.add(node)
566
566
567 @classmethod
567 @classmethod
568 def _filename(cls, repo):
568 def _filename(cls, repo):
569 """name of a branchcache file for a given repo or repoview"""
569 """name of a branchcache file for a given repo or repoview"""
570 filename = cls._base_filename
570 filename = cls._base_filename
571 assert filename is not None
571 assert filename is not None
572 if repo.filtername:
572 if repo.filtername:
573 filename = b'%s-%s' % (filename, repo.filtername)
573 filename = b'%s-%s' % (filename, repo.filtername)
574 return filename
574 return filename
575
575
576 def inherit_for(self, repo):
576 def inherit_for(self, repo):
577 """return a deep copy of the branchcache object"""
577 """return a deep copy of the branchcache object"""
578 assert repo.filtername != self._filtername
578 assert repo.filtername != self._filtername
579 other = type(self)(
579 other = type(self)(
580 repo=repo,
580 repo=repo,
581 # we always do a shally copy of self._entries, and the values is
581 # we always do a shally copy of self._entries, and the values is
582 # always replaced, so no need to deepcopy until the above remains
582 # always replaced, so no need to deepcopy until the above remains
583 # true.
583 # true.
584 entries=self._entries,
584 entries=self._entries,
585 tipnode=self.tipnode,
585 tipnode=self.tipnode,
586 tiprev=self.tiprev,
586 tiprev=self.tiprev,
587 key_hashes=self.key_hashes,
587 key_hashes=self.key_hashes,
588 closednodes=set(self._closednodes),
588 closednodes=set(self._closednodes),
589 verify_node=self._verify_node,
589 verify_node=self._verify_node,
590 inherited=True,
590 inherited=True,
591 )
591 )
592 # also copy information about the current verification state
592 # also copy information about the current verification state
593 other._verifiedbranches = set(self._verifiedbranches)
593 other._verifiedbranches = set(self._verifiedbranches)
594 return other
594 return other
595
595
596 def sync_disk(self, repo):
596 def sync_disk(self, repo):
597 """synchronise the on disk file with the cache state
597 """synchronise the on disk file with the cache state
598
598
599 If new value specific to this filter level need to be written, the file
599 If new value specific to this filter level need to be written, the file
600 will be updated, if the state of the branchcache is inherited from a
600 will be updated, if the state of the branchcache is inherited from a
601 subset, any stalled on disk file will be deleted.
601 subset, any stalled on disk file will be deleted.
602
602
603 That method does nothing if there is nothing to do.
603 That method does nothing if there is nothing to do.
604 """
604 """
605 if self._state == STATE_DIRTY:
605 if self._state == STATE_DIRTY:
606 self.write(repo)
606 self.write(repo)
607 elif self._state == STATE_INHERITED:
607 elif self._state == STATE_INHERITED:
608 filename = self._filename(repo)
608 filename = self._filename(repo)
609 repo.cachevfs.tryunlink(filename)
609 repo.cachevfs.tryunlink(filename)
610
610
611 def write(self, repo):
611 def write(self, repo):
612 assert self._filtername == repo.filtername, (
612 assert self._filtername == repo.filtername, (
613 self._filtername,
613 self._filtername,
614 repo.filtername,
614 repo.filtername,
615 )
615 )
616 assert self._state == STATE_DIRTY, self._state
616 assert self._state == STATE_DIRTY, self._state
617 # This method should not be called during an open transaction
617 # This method should not be called during an open transaction
618 tr = repo.currenttransaction()
618 tr = repo.currenttransaction()
619 if not getattr(tr, 'finalized', True):
619 if not getattr(tr, 'finalized', True):
620 msg = "writing branchcache in the middle of a transaction"
620 msg = "writing branchcache in the middle of a transaction"
621 raise error.ProgrammingError(msg)
621 raise error.ProgrammingError(msg)
622 try:
622 try:
623 filename = self._filename(repo)
623 filename = self._filename(repo)
624 with repo.cachevfs(filename, b"w", atomictemp=True) as f:
624 with repo.cachevfs(filename, b"w", atomictemp=True) as f:
625 self._write_header(f)
625 self._write_header(f)
626 nodecount = self._write_heads(repo, f)
626 nodecount = self._write_heads(repo, f)
627 repo.ui.log(
627 repo.ui.log(
628 b'branchcache',
628 b'branchcache',
629 b'wrote %s with %d labels and %d nodes\n',
629 b'wrote %s with %d labels and %d nodes\n',
630 _branchcachedesc(repo),
630 _branchcachedesc(repo),
631 len(self._entries),
631 len(self._entries),
632 nodecount,
632 nodecount,
633 )
633 )
634 self._state = STATE_CLEAN
634 self._state = STATE_CLEAN
635 except (IOError, OSError, error.Abort) as inst:
635 except (IOError, OSError, error.Abort) as inst:
636 # Abort may be raised by read only opener, so log and continue
636 # Abort may be raised by read only opener, so log and continue
637 repo.ui.debug(
637 repo.ui.debug(
638 b"couldn't write branch cache: %s\n"
638 b"couldn't write branch cache: %s\n"
639 % stringutil.forcebytestr(inst)
639 % stringutil.forcebytestr(inst)
640 )
640 )
641
641
642 def _write_header(self, fp) -> None:
642 def _write_header(self, fp) -> None:
643 raise NotImplementedError
643 raise NotImplementedError
644
644
645 def _write_heads(self, repo, fp) -> int:
645 def _write_heads(self, repo, fp) -> int:
646 """write list of heads to a file
646 """write list of heads to a file
647
647
648 Return the number of heads written."""
648 Return the number of heads written."""
649 nodecount = 0
649 nodecount = 0
650 for label, nodes in sorted(self._entries.items()):
650 for label, nodes in sorted(self._entries.items()):
651 label = encoding.fromlocal(label)
651 label = encoding.fromlocal(label)
652 for node in nodes:
652 for node in nodes:
653 nodecount += 1
653 nodecount += 1
654 if node in self._closednodes:
654 if node in self._closednodes:
655 state = b'c'
655 state = b'c'
656 else:
656 else:
657 state = b'o'
657 state = b'o'
658 fp.write(b"%s %s %s\n" % (hex(node), state, label))
658 fp.write(b"%s %s %s\n" % (hex(node), state, label))
659 return nodecount
659 return nodecount
660
660
661 def _verifybranch(self, branch):
661 def _verifybranch(self, branch):
662 """verify head nodes for the given branch."""
662 """verify head nodes for the given branch."""
663 if not self._verify_node:
663 if not self._verify_node:
664 return
664 return
665 if branch not in self._entries or branch in self._verifiedbranches:
665 if branch not in self._entries or branch in self._verifiedbranches:
666 return
666 return
667 assert self._hasnode is not None
667 assert self._hasnode is not None
668 for n in self._entries[branch]:
668 for n in self._entries[branch]:
669 if not self._hasnode(n):
669 if not self._hasnode(n):
670 _unknownnode(n)
670 _unknownnode(n)
671
671
672 self._verifiedbranches.add(branch)
672 self._verifiedbranches.add(branch)
673
673
674 def _verifyall(self):
674 def _verifyall(self):
675 """verifies nodes of all the branches"""
675 """verifies nodes of all the branches"""
676 for b in self._entries.keys():
676 for b in self._entries.keys():
677 if b not in self._verifiedbranches:
677 if b not in self._verifiedbranches:
678 self._verifybranch(b)
678 self._verifybranch(b)
679
679
680 def __getitem__(self, key):
680 def __getitem__(self, key):
681 self._verifybranch(key)
681 self._verifybranch(key)
682 return super().__getitem__(key)
682 return super().__getitem__(key)
683
683
684 def __contains__(self, key):
684 def __contains__(self, key):
685 self._verifybranch(key)
685 self._verifybranch(key)
686 return super().__contains__(key)
686 return super().__contains__(key)
687
687
688 def iteritems(self):
688 def iteritems(self):
689 self._verifyall()
689 self._verifyall()
690 return super().iteritems()
690 return super().iteritems()
691
691
692 items = iteritems
692 items = iteritems
693
693
694 def iterheads(self):
694 def iterheads(self):
695 """returns all the heads"""
695 """returns all the heads"""
696 self._verifyall()
696 self._verifyall()
697 return super().iterheads()
697 return super().iterheads()
698
698
699 def hasbranch(self, label):
699 def hasbranch(self, label):
700 """checks whether a branch of this name exists or not"""
700 """checks whether a branch of this name exists or not"""
701 self._verifybranch(label)
701 self._verifybranch(label)
702 return super().hasbranch(label)
702 return super().hasbranch(label)
703
703
704 def branchheads(self, branch, closed=False):
704 def branchheads(self, branch, closed=False):
705 self._verifybranch(branch)
705 self._verifybranch(branch)
706 return super().branchheads(branch, closed=closed)
706 return super().branchheads(branch, closed=closed)
707
707
708 def update(self, repo, revgen):
708 def update(self, repo, revgen):
709 assert self._filtername == repo.filtername, (
709 assert self._filtername == repo.filtername, (
710 self._filtername,
710 self._filtername,
711 repo.filtername,
711 repo.filtername,
712 )
712 )
713 cl = repo.changelog
713 cl = repo.changelog
714 max_rev = super().update(repo, revgen)
714 max_rev = super().update(repo, revgen)
715 # new tip revision which we found after iterating items from new
715 # new tip revision which we found after iterating items from new
716 # branches
716 # branches
717 if max_rev is not None and max_rev > self.tiprev:
717 if max_rev is not None and max_rev > self.tiprev:
718 self.tiprev = max_rev
718 self.tiprev = max_rev
719 self.tipnode = cl.node(max_rev)
719 self.tipnode = cl.node(max_rev)
720 else:
720 else:
721 # We should not be here is if this is false
721 # We should not be here is if this is false
722 assert cl.node(self.tiprev) == self.tipnode
722 assert cl.node(self.tiprev) == self.tipnode
723
723
724 if not self.validfor(repo):
724 if not self.validfor(repo):
725 # the tiprev and tipnode should be aligned, so if the current repo
725 # the tiprev and tipnode should be aligned, so if the current repo
726 # is not seens as valid this is because old cache key is now
726 # is not seens as valid this is because old cache key is now
727 # invalid for the repo.
727 # invalid for the repo.
728 #
728 #
729 # However. we've just updated the cache and we assume it's valid,
729 # However. we've just updated the cache and we assume it's valid,
730 # so let's make the cache key valid as well by recomputing it from
730 # so let's make the cache key valid as well by recomputing it from
731 # the cached data
731 # the cached data
732 self.key_hashes = self._compute_key_hashes(repo)
732 self.key_hashes = self._compute_key_hashes(repo)
733 self.filteredhash = scmutil.combined_filtered_and_obsolete_hash(
733 self.filteredhash = scmutil.combined_filtered_and_obsolete_hash(
734 repo,
734 repo,
735 self.tiprev,
735 self.tiprev,
736 )
736 )
737
737
738 self._state = STATE_DIRTY
738 self._state = STATE_DIRTY
739 tr = repo.currenttransaction()
739 tr = repo.currenttransaction()
740 if getattr(tr, 'finalized', True):
740 if getattr(tr, 'finalized', True):
741 # Avoid premature writing.
741 # Avoid premature writing.
742 #
742 #
743 # (The cache warming setup by localrepo will update the file later.)
743 # (The cache warming setup by localrepo will update the file later.)
744 self.write(repo)
744 self.write(repo)
745
745
746
746
747 def branch_cache_from_file(repo) -> Optional[_LocalBranchCache]:
747 def branch_cache_from_file(repo) -> Optional[_LocalBranchCache]:
748 """Build a branch cache from on-disk data if possible
748 """Build a branch cache from on-disk data if possible
749
749
750 Return a branch cache of the right format depending of the repository.
750 Return a branch cache of the right format depending of the repository.
751 """
751 """
752 if repo.ui.configbool(b"experimental", b"branch-cache-v3"):
752 if repo.ui.configbool(b"experimental", b"branch-cache-v3"):
753 return BranchCacheV3.fromfile(repo)
753 return BranchCacheV3.fromfile(repo)
754 else:
754 else:
755 return BranchCacheV2.fromfile(repo)
755 return BranchCacheV2.fromfile(repo)
756
756
757
757
758 def new_branch_cache(repo, *args, **kwargs):
758 def new_branch_cache(repo, *args, **kwargs):
759 """Build a new branch cache from argument
759 """Build a new branch cache from argument
760
760
761 Return a branch cache of the right format depending of the repository.
761 Return a branch cache of the right format depending of the repository.
762 """
762 """
763 if repo.ui.configbool(b"experimental", b"branch-cache-v3"):
763 if repo.ui.configbool(b"experimental", b"branch-cache-v3"):
764 return BranchCacheV3(repo, *args, **kwargs)
764 return BranchCacheV3(repo, *args, **kwargs)
765 else:
765 else:
766 return BranchCacheV2(repo, *args, **kwargs)
766 return BranchCacheV2(repo, *args, **kwargs)
767
767
768
768
769 class BranchCacheV2(_LocalBranchCache):
769 class BranchCacheV2(_LocalBranchCache):
770 """a branch cache using version 2 of the format on disk
770 """a branch cache using version 2 of the format on disk
771
771
772 The cache is serialized on disk in the following format:
772 The cache is serialized on disk in the following format:
773
773
774 <tip hex node> <tip rev number> [optional filtered repo hex hash]
774 <tip hex node> <tip rev number> [optional filtered repo hex hash]
775 <branch head hex node> <open/closed state> <branch name>
775 <branch head hex node> <open/closed state> <branch name>
776 <branch head hex node> <open/closed state> <branch name>
776 <branch head hex node> <open/closed state> <branch name>
777 ...
777 ...
778
778
779 The first line is used to check if the cache is still valid. If the
779 The first line is used to check if the cache is still valid. If the
780 branch cache is for a filtered repo view, an optional third hash is
780 branch cache is for a filtered repo view, an optional third hash is
781 included that hashes the hashes of all filtered and obsolete revisions.
781 included that hashes the hashes of all filtered and obsolete revisions.
782
782
783 The open/closed state is represented by a single letter 'o' or 'c'.
783 The open/closed state is represented by a single letter 'o' or 'c'.
784 This field can be used to avoid changelog reads when determining if a
784 This field can be used to avoid changelog reads when determining if a
785 branch head closes a branch or not.
785 branch head closes a branch or not.
786 """
786 """
787
787
788 _base_filename = b"branch2"
788 _base_filename = b"branch2"
789
789
790 @classmethod
790 @classmethod
791 def _load_header(cls, repo, lineiter) -> "dict[str, Any]":
791 def _load_header(cls, repo, lineiter) -> "dict[str, Any]":
792 """parse the head of a branchmap file
792 """parse the head of a branchmap file
793
793
794 return parameters to pass to a newly created class instance.
794 return parameters to pass to a newly created class instance.
795 """
795 """
796 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
796 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
797 last, lrev = cachekey[:2]
797 last, lrev = cachekey[:2]
798 last, lrev = bin(last), int(lrev)
798 last, lrev = bin(last), int(lrev)
799 filteredhash = ()
799 filteredhash = ()
800 if len(cachekey) > 2:
800 if len(cachekey) > 2:
801 filteredhash = (bin(cachekey[2]),)
801 filteredhash = (bin(cachekey[2]),)
802 return {
802 return {
803 "tipnode": last,
803 "tipnode": last,
804 "tiprev": lrev,
804 "tiprev": lrev,
805 "key_hashes": filteredhash,
805 "key_hashes": filteredhash,
806 }
806 }
807
807
808 def _write_header(self, fp) -> None:
808 def _write_header(self, fp) -> None:
809 """write the branch cache header to a file"""
809 """write the branch cache header to a file"""
810 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
810 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
811 if self.key_hashes:
811 if self.key_hashes:
812 cachekey.append(hex(self.key_hashes[0]))
812 cachekey.append(hex(self.key_hashes[0]))
813 fp.write(b" ".join(cachekey) + b'\n')
813 fp.write(b" ".join(cachekey) + b'\n')
814
814
815 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
815 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
816 """return the cache key hashes that match this repoview state"""
816 """return the cache key hashes that match this repoview state"""
817 filtered_hash = scmutil.combined_filtered_and_obsolete_hash(
817 filtered_hash = scmutil.combined_filtered_and_obsolete_hash(
818 repo,
818 repo,
819 self.tiprev,
819 self.tiprev,
820 needobsolete=True,
820 needobsolete=True,
821 )
821 )
822 keys: Tuple[bytes] = cast(Tuple[bytes], ())
822 keys: Tuple[bytes] = cast(Tuple[bytes], ())
823 if filtered_hash is not None:
823 if filtered_hash is not None:
824 keys: Tuple[bytes] = (filtered_hash,)
824 keys: Tuple[bytes] = (filtered_hash,)
825 return keys
825 return keys
826
826
827
827
828 class BranchCacheV3(_LocalBranchCache):
828 class BranchCacheV3(_LocalBranchCache):
829 """a branch cache using version 3 of the format on disk
829 """a branch cache using version 3 of the format on disk
830
830
831 This version is still EXPERIMENTAL and the format is subject to changes.
831 This version is still EXPERIMENTAL and the format is subject to changes.
832
832
833 The cache is serialized on disk in the following format:
833 The cache is serialized on disk in the following format:
834
834
835 <cache-key-xxx>=<xxx-value> <cache-key-yyy>=<yyy-value> […]
835 <cache-key-xxx>=<xxx-value> <cache-key-yyy>=<yyy-value> […]
836 <branch head hex node> <open/closed state> <branch name>
836 <branch head hex node> <open/closed state> <branch name>
837 <branch head hex node> <open/closed state> <branch name>
837 <branch head hex node> <open/closed state> <branch name>
838 ...
838 ...
839
839
840 The first line is used to check if the cache is still valid. It is a series
840 The first line is used to check if the cache is still valid. It is a series
841 of key value pair. The following key are recognized:
841 of key value pair. The following key are recognized:
842
842
843 - tip-rev: the rev-num of the tip-most revision seen by this cache
843 - tip-rev: the rev-num of the tip-most revision seen by this cache
844 - tip-node: the node-id of the tip-most revision sen by this cache
844 - tip-node: the node-id of the tip-most revision sen by this cache
845 - filtered-hash: the hash of all filtered revisions (before tip-rev)
845 - filtered-hash: the hash of all filtered revisions (before tip-rev)
846 ignored by this cache.
846 ignored by this cache.
847 - obsolete-hash: the hash of all non-filtered obsolete revisions (before
847 - obsolete-hash: the hash of all non-filtered obsolete revisions (before
848 tip-rev) ignored by this cache.
848 tip-rev) ignored by this cache.
849
849
850 The tip-rev is used to know how far behind the value in the file are
850 The tip-rev is used to know how far behind the value in the file are
851 compared to the current repository state.
851 compared to the current repository state.
852
852
853 The tip-node, filtered-hash and obsolete-hash are used to detect if this
853 The tip-node, filtered-hash and obsolete-hash are used to detect if this
854 cache can be used for this repository state at all.
854 cache can be used for this repository state at all.
855
855
856 The open/closed state is represented by a single letter 'o' or 'c'.
856 The open/closed state is represented by a single letter 'o' or 'c'.
857 This field can be used to avoid changelog reads when determining if a
857 This field can be used to avoid changelog reads when determining if a
858 branch head closes a branch or not.
858 branch head closes a branch or not.
859
859
860 Topological heads are not included in the listing and should be dispatched
860 Topological heads are not included in the listing and should be dispatched
861 on the right branch at read time. Obsolete topological heads should be
861 on the right branch at read time. Obsolete topological heads should be
862 ignored.
862 ignored.
863 """
863 """
864
864
865 _base_filename = b"branch3-exp"
865 _base_filename = b"branch3-exp"
866 _default_key_hashes = (None, None)
866 _default_key_hashes = (None, None)
867
867
868 def __init__(self, *args, pure_topo_branch=None, **kwargs):
868 def __init__(self, *args, pure_topo_branch=None, **kwargs):
869 super().__init__(*args, **kwargs)
869 super().__init__(*args, **kwargs)
870 self._pure_topo_branch = pure_topo_branch
870 self._pure_topo_branch = pure_topo_branch
871 self._needs_populate = self._pure_topo_branch is not None
871 self._needs_populate = self._pure_topo_branch is not None
872
872
873 def inherit_for(self, repo):
873 def inherit_for(self, repo):
874 new = super().inherit_for(repo)
874 new = super().inherit_for(repo)
875 new._pure_topo_branch = self._pure_topo_branch
875 new._pure_topo_branch = self._pure_topo_branch
876 new._needs_populate = self._needs_populate
876 new._needs_populate = self._needs_populate
877 return new
877 return new
878
878
879 def _get_topo_heads(self, repo):
879 def _get_topo_heads(self, repo):
880 """returns the topological head of a repoview content up to self.tiprev"""
880 """returns the topological head of a repoview content up to self.tiprev"""
881 cl = repo.changelog
881 cl = repo.changelog
882 if self.tiprev == nullrev:
882 if self.tiprev == nullrev:
883 return []
883 return []
884 elif self.tiprev == cl.tiprev():
884 elif self.tiprev == cl.tiprev():
885 return cl.headrevs()
885 return cl.headrevs()
886 else:
886 else:
887 # XXX passing tiprev as ceiling of cl.headrevs could be faster
887 # XXX passing tiprev as ceiling of cl.headrevs could be faster
888 heads = cl.headrevs(cl.revs(stop=self.tiprev))
888 heads = cl.headrevs(cl.revs(stop=self.tiprev))
889 return heads
889 return heads
890
890
891 def _write_header(self, fp) -> None:
891 def _write_header(self, fp) -> None:
892 cache_keys = {
892 cache_keys = {
893 b"tip-node": hex(self.tipnode),
893 b"tip-node": hex(self.tipnode),
894 b"tip-rev": b'%d' % self.tiprev,
894 b"tip-rev": b'%d' % self.tiprev,
895 }
895 }
896 if self.key_hashes:
896 if self.key_hashes:
897 if self.key_hashes[0] is not None:
897 if self.key_hashes[0] is not None:
898 cache_keys[b"filtered-hash"] = hex(self.key_hashes[0])
898 cache_keys[b"filtered-hash"] = hex(self.key_hashes[0])
899 if self.key_hashes[1] is not None:
899 if self.key_hashes[1] is not None:
900 cache_keys[b"obsolete-hash"] = hex(self.key_hashes[1])
900 cache_keys[b"obsolete-hash"] = hex(self.key_hashes[1])
901 if self._pure_topo_branch is not None:
901 if self._pure_topo_branch is not None:
902 cache_keys[b"topo-mode"] = b"pure"
902 cache_keys[b"topo-mode"] = b"pure"
903 pieces = (b"%s=%s" % i for i in sorted(cache_keys.items()))
903 pieces = (b"%s=%s" % i for i in sorted(cache_keys.items()))
904 fp.write(b" ".join(pieces) + b'\n')
904 fp.write(b" ".join(pieces) + b'\n')
905 if self._pure_topo_branch is not None:
905 if self._pure_topo_branch is not None:
906 label = encoding.fromlocal(self._pure_topo_branch)
906 label = encoding.fromlocal(self._pure_topo_branch)
907 fp.write(label + b'\n')
907 fp.write(label + b'\n')
908
908
909 def _write_heads(self, repo, fp) -> int:
909 def _write_heads(self, repo, fp) -> int:
910 """write list of heads to a file
910 """write list of heads to a file
911
911
912 Return the number of heads written."""
912 Return the number of heads written."""
913 nodecount = 0
913 nodecount = 0
914 topo_heads = None
914 topo_heads = None
915 if self._pure_topo_branch is None:
915 if self._pure_topo_branch is None:
916 topo_heads = set(self._get_topo_heads(repo))
916 topo_heads = set(self._get_topo_heads(repo))
917 to_rev = repo.changelog.index.rev
917 to_rev = repo.changelog.index.rev
918 for label, nodes in sorted(self._entries.items()):
918 for label, nodes in sorted(self._entries.items()):
919 if label == self._pure_topo_branch:
919 if label == self._pure_topo_branch:
920 # not need to write anything the header took care of that
920 # not need to write anything the header took care of that
921 continue
921 continue
922 label = encoding.fromlocal(label)
922 label = encoding.fromlocal(label)
923 for node in nodes:
923 for node in nodes:
924 if topo_heads is not None:
924 if topo_heads is not None:
925 rev = to_rev(node)
925 rev = to_rev(node)
926 if rev in topo_heads:
926 if rev in topo_heads:
927 continue
927 continue
928 if node in self._closednodes:
928 if node in self._closednodes:
929 state = b'c'
929 state = b'c'
930 else:
930 else:
931 state = b'o'
931 state = b'o'
932 nodecount += 1
932 nodecount += 1
933 fp.write(b"%s %s %s\n" % (hex(node), state, label))
933 fp.write(b"%s %s %s\n" % (hex(node), state, label))
934 return nodecount
934 return nodecount
935
935
936 @classmethod
936 @classmethod
937 def _load_header(cls, repo, lineiter):
937 def _load_header(cls, repo, lineiter):
938 header_line = next(lineiter)
938 header_line = next(lineiter)
939 pieces = header_line.rstrip(b'\n').split(b" ")
939 pieces = header_line.rstrip(b'\n').split(b" ")
940 for p in pieces:
941 if b'=' not in p:
942 msg = b"invalid header_line: %r" % header_line
943 raise ValueError(msg)
940 cache_keys = dict(p.split(b'=', 1) for p in pieces)
944 cache_keys = dict(p.split(b'=', 1) for p in pieces)
941
945
942 args = {}
946 args = {}
943 filtered_hash = None
947 filtered_hash = None
944 obsolete_hash = None
948 obsolete_hash = None
945 has_pure_topo_heads = False
949 has_pure_topo_heads = False
946 for k, v in cache_keys.items():
950 for k, v in cache_keys.items():
947 if k == b"tip-rev":
951 if k == b"tip-rev":
948 args["tiprev"] = int(v)
952 args["tiprev"] = int(v)
949 elif k == b"tip-node":
953 elif k == b"tip-node":
950 args["tipnode"] = bin(v)
954 args["tipnode"] = bin(v)
951 elif k == b"filtered-hash":
955 elif k == b"filtered-hash":
952 filtered_hash = bin(v)
956 filtered_hash = bin(v)
953 elif k == b"obsolete-hash":
957 elif k == b"obsolete-hash":
954 obsolete_hash = bin(v)
958 obsolete_hash = bin(v)
955 elif k == b"topo-mode":
959 elif k == b"topo-mode":
956 if v == b"pure":
960 if v == b"pure":
957 has_pure_topo_heads = True
961 has_pure_topo_heads = True
958 else:
962 else:
959 msg = b"unknown topo-mode: %r" % v
963 msg = b"unknown topo-mode: %r" % v
960 raise ValueError(msg)
964 raise ValueError(msg)
961 else:
965 else:
962 msg = b"unknown cache key: %r" % k
966 msg = b"unknown cache key: %r" % k
963 raise ValueError(msg)
967 raise ValueError(msg)
964 args["key_hashes"] = (filtered_hash, obsolete_hash)
968 args["key_hashes"] = (filtered_hash, obsolete_hash)
965 if has_pure_topo_heads:
969 if has_pure_topo_heads:
966 pure_line = next(lineiter).rstrip(b'\n')
970 pure_line = next(lineiter).rstrip(b'\n')
967 args["pure_topo_branch"] = encoding.tolocal(pure_line)
971 args["pure_topo_branch"] = encoding.tolocal(pure_line)
968 return args
972 return args
969
973
970 def _load_heads(self, repo, lineiter):
974 def _load_heads(self, repo, lineiter):
971 """fully loads the branchcache by reading from the file using the line
975 """fully loads the branchcache by reading from the file using the line
972 iterator passed"""
976 iterator passed"""
973 super()._load_heads(repo, lineiter)
977 super()._load_heads(repo, lineiter)
974 if self._pure_topo_branch is not None:
978 if self._pure_topo_branch is not None:
975 # no need to read the repository heads, we know their value already.
979 # no need to read the repository heads, we know their value already.
976 return
980 return
977 cl = repo.changelog
981 cl = repo.changelog
978 getbranchinfo = repo.revbranchcache().branchinfo
982 getbranchinfo = repo.revbranchcache().branchinfo
979 obsrevs = obsolete.getrevs(repo, b'obsolete')
983 obsrevs = obsolete.getrevs(repo, b'obsolete')
980 to_node = cl.node
984 to_node = cl.node
981 touched_branch = set()
985 touched_branch = set()
982 for head in self._get_topo_heads(repo):
986 for head in self._get_topo_heads(repo):
983 if head in obsrevs:
987 if head in obsrevs:
984 continue
988 continue
985 node = to_node(head)
989 node = to_node(head)
986 branch, closed = getbranchinfo(head)
990 branch, closed = getbranchinfo(head)
987 self._entries.setdefault(branch, []).append(node)
991 self._entries.setdefault(branch, []).append(node)
988 if closed:
992 if closed:
989 self._closednodes.add(node)
993 self._closednodes.add(node)
990 touched_branch.add(branch)
994 touched_branch.add(branch)
991 to_rev = cl.index.rev
995 to_rev = cl.index.rev
992 for branch in touched_branch:
996 for branch in touched_branch:
993 self._entries[branch].sort(key=to_rev)
997 self._entries[branch].sort(key=to_rev)
994
998
995 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
999 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
996 """return the cache key hashes that match this repoview state"""
1000 """return the cache key hashes that match this repoview state"""
997 return scmutil.filtered_and_obsolete_hash(
1001 return scmutil.filtered_and_obsolete_hash(
998 repo,
1002 repo,
999 self.tiprev,
1003 self.tiprev,
1000 )
1004 )
1001
1005
1002 def _process_new(
1006 def _process_new(
1003 self,
1007 self,
1004 repo,
1008 repo,
1005 newbranches,
1009 newbranches,
1006 new_closed,
1010 new_closed,
1007 obs_ignored,
1011 obs_ignored,
1008 max_rev,
1012 max_rev,
1009 ) -> None:
1013 ) -> None:
1010 if (
1014 if (
1011 # note: the check about `obs_ignored` is too strict as the
1015 # note: the check about `obs_ignored` is too strict as the
1012 # obsolete revision could be non-topological, but lets keep
1016 # obsolete revision could be non-topological, but lets keep
1013 # things simple for now
1017 # things simple for now
1014 #
1018 #
1015 # The same apply to `new_closed` if the closed changeset are
1019 # The same apply to `new_closed` if the closed changeset are
1016 # not a head, we don't care that it is closed, but lets keep
1020 # not a head, we don't care that it is closed, but lets keep
1017 # things simple here too.
1021 # things simple here too.
1018 not (obs_ignored or new_closed)
1022 not (obs_ignored or new_closed)
1019 and (
1023 and (
1020 not newbranches
1024 not newbranches
1021 or (
1025 or (
1022 len(newbranches) == 1
1026 len(newbranches) == 1
1023 and (
1027 and (
1024 self.tiprev == nullrev
1028 self.tiprev == nullrev
1025 or self._pure_topo_branch in newbranches
1029 or self._pure_topo_branch in newbranches
1026 )
1030 )
1027 )
1031 )
1028 )
1032 )
1029 ):
1033 ):
1030 if newbranches:
1034 if newbranches:
1031 assert len(newbranches) == 1
1035 assert len(newbranches) == 1
1032 self._pure_topo_branch = list(newbranches.keys())[0]
1036 self._pure_topo_branch = list(newbranches.keys())[0]
1033 self._needs_populate = True
1037 self._needs_populate = True
1034 self._entries.pop(self._pure_topo_branch, None)
1038 self._entries.pop(self._pure_topo_branch, None)
1035 return
1039 return
1036
1040
1037 self._ensure_populated(repo)
1041 self._ensure_populated(repo)
1038 self._pure_topo_branch = None
1042 self._pure_topo_branch = None
1039 super()._process_new(
1043 super()._process_new(
1040 repo,
1044 repo,
1041 newbranches,
1045 newbranches,
1042 new_closed,
1046 new_closed,
1043 obs_ignored,
1047 obs_ignored,
1044 max_rev,
1048 max_rev,
1045 )
1049 )
1046
1050
1047 def _ensure_populated(self, repo):
1051 def _ensure_populated(self, repo):
1048 """make sure any lazily loaded values are fully populated"""
1052 """make sure any lazily loaded values are fully populated"""
1049 if self._needs_populate:
1053 if self._needs_populate:
1050 assert self._pure_topo_branch is not None
1054 assert self._pure_topo_branch is not None
1051 cl = repo.changelog
1055 cl = repo.changelog
1052 to_node = cl.node
1056 to_node = cl.node
1053 topo_heads = self._get_topo_heads(repo)
1057 topo_heads = self._get_topo_heads(repo)
1054 heads = [to_node(r) for r in topo_heads]
1058 heads = [to_node(r) for r in topo_heads]
1055 self._entries[self._pure_topo_branch] = heads
1059 self._entries[self._pure_topo_branch] = heads
1056 self._needs_populate = False
1060 self._needs_populate = False
1057
1061
1058 def _detect_pure_topo(self, repo) -> None:
1062 def _detect_pure_topo(self, repo) -> None:
1059 if self._pure_topo_branch is not None:
1063 if self._pure_topo_branch is not None:
1060 # we are pure topological already
1064 # we are pure topological already
1061 return
1065 return
1062 to_node = repo.changelog.node
1066 to_node = repo.changelog.node
1063 topo_heads = [to_node(r) for r in self._get_topo_heads(repo)]
1067 topo_heads = [to_node(r) for r in self._get_topo_heads(repo)]
1064 if any(n in self._closednodes for n in topo_heads):
1068 if any(n in self._closednodes for n in topo_heads):
1065 return
1069 return
1066 for branch, heads in self._entries.items():
1070 for branch, heads in self._entries.items():
1067 if heads == topo_heads:
1071 if heads == topo_heads:
1068 self._pure_topo_branch = branch
1072 self._pure_topo_branch = branch
1069 break
1073 break
1070
1074
1071
1075
1072 class remotebranchcache(_BaseBranchCache):
1076 class remotebranchcache(_BaseBranchCache):
1073 """Branchmap info for a remote connection, should not write locally"""
1077 """Branchmap info for a remote connection, should not write locally"""
1074
1078
1075 def __init__(
1079 def __init__(
1076 self,
1080 self,
1077 repo: "localrepo.localrepository",
1081 repo: "localrepo.localrepository",
1078 entries: Union[
1082 entries: Union[
1079 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
1083 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
1080 ] = (),
1084 ] = (),
1081 closednodes: Optional[Set[bytes]] = None,
1085 closednodes: Optional[Set[bytes]] = None,
1082 ) -> None:
1086 ) -> None:
1083 super().__init__(repo=repo, entries=entries, closed_nodes=closednodes)
1087 super().__init__(repo=repo, entries=entries, closed_nodes=closednodes)
General Comments 0
You need to be logged in to leave comments. Login now