##// END OF EJS Templates
branchmap: pre-filter topological heads before ancestors based filtering...
Pierre-Yves David -
r22357:9c3c3dc1 default
parent child Browse files
Show More
@@ -1,280 +1,287
1 1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import bin, hex, nullid, nullrev
9 9 import encoding
10 10 import util
11 11 import time
12 12
13 13 def _filename(repo):
14 14 """name of a branchcache file for a given repo or repoview"""
15 15 filename = "cache/branch2"
16 16 if repo.filtername:
17 17 filename = '%s-%s' % (filename, repo.filtername)
18 18 return filename
19 19
20 20 def read(repo):
21 21 try:
22 22 f = repo.opener(_filename(repo))
23 23 lines = f.read().split('\n')
24 24 f.close()
25 25 except (IOError, OSError):
26 26 return None
27 27
28 28 try:
29 29 cachekey = lines.pop(0).split(" ", 2)
30 30 last, lrev = cachekey[:2]
31 31 last, lrev = bin(last), int(lrev)
32 32 filteredhash = None
33 33 if len(cachekey) > 2:
34 34 filteredhash = bin(cachekey[2])
35 35 partial = branchcache(tipnode=last, tiprev=lrev,
36 36 filteredhash=filteredhash)
37 37 if not partial.validfor(repo):
38 38 # invalidate the cache
39 39 raise ValueError('tip differs')
40 40 for l in lines:
41 41 if not l:
42 42 continue
43 43 node, state, label = l.split(" ", 2)
44 44 if state not in 'oc':
45 45 raise ValueError('invalid branch state')
46 46 label = encoding.tolocal(label.strip())
47 47 if not node in repo:
48 48 raise ValueError('node %s does not exist' % node)
49 49 node = bin(node)
50 50 partial.setdefault(label, []).append(node)
51 51 if state == 'c':
52 52 partial._closednodes.add(node)
53 53 except KeyboardInterrupt:
54 54 raise
55 55 except Exception, inst:
56 56 if repo.ui.debugflag:
57 57 msg = 'invalid branchheads cache'
58 58 if repo.filtername is not None:
59 59 msg += ' (%s)' % repo.filtername
60 60 msg += ': %s\n'
61 61 repo.ui.debug(msg % inst)
62 62 partial = None
63 63 return partial
64 64
65 65 ### Nearest subset relation
66 66 # Nearest subset of filter X is a filter Y so that:
67 67 # * Y is included in X,
68 68 # * X - Y is as small as possible.
69 69 # This create and ordering used for branchmap purpose.
70 70 # the ordering may be partial
71 71 subsettable = {None: 'visible',
72 72 'visible': 'served',
73 73 'served': 'immutable',
74 74 'immutable': 'base'}
75 75
76 76 def updatecache(repo):
77 77 cl = repo.changelog
78 78 filtername = repo.filtername
79 79 partial = repo._branchcaches.get(filtername)
80 80
81 81 revs = []
82 82 if partial is None or not partial.validfor(repo):
83 83 partial = read(repo)
84 84 if partial is None:
85 85 subsetname = subsettable.get(filtername)
86 86 if subsetname is None:
87 87 partial = branchcache()
88 88 else:
89 89 subset = repo.filtered(subsetname)
90 90 partial = subset.branchmap().copy()
91 91 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
92 92 revs.extend(r for r in extrarevs if r <= partial.tiprev)
93 93 revs.extend(cl.revs(start=partial.tiprev + 1))
94 94 if revs:
95 95 partial.update(repo, revs)
96 96 partial.write(repo)
97 97 assert partial.validfor(repo), filtername
98 98 repo._branchcaches[repo.filtername] = partial
99 99
100 100 class branchcache(dict):
101 101 """A dict like object that hold branches heads cache.
102 102
103 103 This cache is used to avoid costly computations to determine all the
104 104 branch heads of a repo.
105 105
106 106 The cache is serialized on disk in the following format:
107 107
108 108 <tip hex node> <tip rev number> [optional filtered repo hex hash]
109 109 <branch head hex node> <open/closed state> <branch name>
110 110 <branch head hex node> <open/closed state> <branch name>
111 111 ...
112 112
113 113 The first line is used to check if the cache is still valid. If the
114 114 branch cache is for a filtered repo view, an optional third hash is
115 115 included that hashes the hashes of all filtered revisions.
116 116
117 117 The open/closed state is represented by a single letter 'o' or 'c'.
118 118 This field can be used to avoid changelog reads when determining if a
119 119 branch head closes a branch or not.
120 120 """
121 121
122 122 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
123 123 filteredhash=None, closednodes=None):
124 124 super(branchcache, self).__init__(entries)
125 125 self.tipnode = tipnode
126 126 self.tiprev = tiprev
127 127 self.filteredhash = filteredhash
128 128 # closednodes is a set of nodes that close their branch. If the branch
129 129 # cache has been updated, it may contain nodes that are no longer
130 130 # heads.
131 131 if closednodes is None:
132 132 self._closednodes = set()
133 133 else:
134 134 self._closednodes = closednodes
135 135
136 136 def _hashfiltered(self, repo):
137 137 """build hash of revision filtered in the current cache
138 138
139 139 Tracking tipnode and tiprev is not enough to ensure validity of the
140 140 cache as they do not help to distinct cache that ignored various
141 141 revision bellow tiprev.
142 142
143 143 To detect such difference, we build a cache of all ignored revisions.
144 144 """
145 145 cl = repo.changelog
146 146 if not cl.filteredrevs:
147 147 return None
148 148 key = None
149 149 revs = sorted(r for r in cl.filteredrevs if r <= self.tiprev)
150 150 if revs:
151 151 s = util.sha1()
152 152 for rev in revs:
153 153 s.update('%s;' % rev)
154 154 key = s.digest()
155 155 return key
156 156
157 157 def validfor(self, repo):
158 158 """Is the cache content valid regarding a repo
159 159
160 160 - False when cached tipnode is unknown or if we detect a strip.
161 161 - True when cache is up to date or a subset of current repo."""
162 162 try:
163 163 return ((self.tipnode == repo.changelog.node(self.tiprev))
164 164 and (self.filteredhash == self._hashfiltered(repo)))
165 165 except IndexError:
166 166 return False
167 167
168 168 def _branchtip(self, heads):
169 169 '''Return tuple with last open head in heads and false,
170 170 otherwise return last closed head and true.'''
171 171 tip = heads[-1]
172 172 closed = True
173 173 for h in reversed(heads):
174 174 if h not in self._closednodes:
175 175 tip = h
176 176 closed = False
177 177 break
178 178 return tip, closed
179 179
180 180 def branchtip(self, branch):
181 181 '''Return the tipmost open head on branch head, otherwise return the
182 182 tipmost closed head on branch.
183 183 Raise KeyError for unknown branch.'''
184 184 return self._branchtip(self[branch])[0]
185 185
186 186 def branchheads(self, branch, closed=False):
187 187 heads = self[branch]
188 188 if not closed:
189 189 heads = [h for h in heads if h not in self._closednodes]
190 190 return heads
191 191
192 192 def iterbranches(self):
193 193 for bn, heads in self.iteritems():
194 194 yield (bn, heads) + self._branchtip(heads)
195 195
196 196 def copy(self):
197 197 """return an deep copy of the branchcache object"""
198 198 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
199 199 self._closednodes)
200 200
201 201 def write(self, repo):
202 202 try:
203 203 f = repo.opener(_filename(repo), "w", atomictemp=True)
204 204 cachekey = [hex(self.tipnode), str(self.tiprev)]
205 205 if self.filteredhash is not None:
206 206 cachekey.append(hex(self.filteredhash))
207 207 f.write(" ".join(cachekey) + '\n')
208 208 nodecount = 0
209 209 for label, nodes in sorted(self.iteritems()):
210 210 for node in nodes:
211 211 nodecount += 1
212 212 if node in self._closednodes:
213 213 state = 'c'
214 214 else:
215 215 state = 'o'
216 216 f.write("%s %s %s\n" % (hex(node), state,
217 217 encoding.fromlocal(label)))
218 218 f.close()
219 219 repo.ui.log('branchcache',
220 220 'wrote %s branch cache with %d labels and %d nodes\n',
221 221 repo.filtername, len(self), nodecount)
222 222 except (IOError, OSError, util.Abort), inst:
223 223 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
224 224 # Abort may be raise by read only opener
225 225 pass
226 226
227 227 def update(self, repo, revgen):
228 228 """Given a branchhead cache, self, that may have extra nodes or be
229 229 missing heads, and a generator of nodes that are strictly a superset of
230 230 heads missing, this function updates self to be correct.
231 231 """
232 232 starttime = time.time()
233 233 cl = repo.changelog
234 234 # collect new branch entries
235 235 newbranches = {}
236 236 getbranchinfo = cl.branchinfo
237 237 for r in revgen:
238 238 branch, closesbranch = getbranchinfo(r)
239 239 newbranches.setdefault(branch, []).append(r)
240 240 if closesbranch:
241 241 self._closednodes.add(cl.node(r))
242
243 # fetch current topological heads to speed up filtering
244 topoheads = set(cl.headrevs())
245
242 246 # if older branchheads are reachable from new ones, they aren't
243 247 # really branchheads. Note checking parents is insufficient:
244 248 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
245 249 for branch, newheadrevs in newbranches.iteritems():
246 250 bheads = self.setdefault(branch, [])
247 251 bheadset = set(cl.rev(node) for node in bheads)
248 252
249 253 # This have been tested True on all internal usage of this function.
250 254 # run it again in case of doubt
251 255 # assert not (set(bheadrevs) & set(newheadrevs))
252 256 newheadrevs.sort()
253 257 bheadset.update(newheadrevs)
254 258
255 259 # This prunes out two kinds of heads - heads that are superseded by
256 260 # a head in newheadrevs, and newheadrevs that are not heads because
257 261 # an existing head is their descendant.
258 ancestors = set(cl.ancestors(newheadrevs, min(bheadset)))
262 uncertain = bheadset - topoheads
263 if uncertain:
264 floorrev = min(uncertain)
265 ancestors = set(cl.ancestors(newheadrevs, floorrev))
259 266 bheadset -= ancestors
260 267 bheadrevs = sorted(bheadset)
261 268 self[branch] = [cl.node(rev) for rev in bheadrevs]
262 269 tiprev = bheadrevs[-1]
263 270 if tiprev > self.tiprev:
264 271 self.tipnode = cl.node(tiprev)
265 272 self.tiprev = tiprev
266 273
267 274 if not self.validfor(repo):
268 275 # cache key are not valid anymore
269 276 self.tipnode = nullid
270 277 self.tiprev = nullrev
271 278 for heads in self.values():
272 279 tiprev = max(cl.rev(node) for node in heads)
273 280 if tiprev > self.tiprev:
274 281 self.tipnode = cl.node(tiprev)
275 282 self.tiprev = tiprev
276 283 self.filteredhash = self._hashfiltered(repo)
277 284
278 285 duration = time.time() - starttime
279 286 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
280 287 repo.filtername, duration)
General Comments 0
You need to be logged in to leave comments. Login now