##// END OF EJS Templates
hbisect: use a revset for ancestor calculation...
David Soria Parra -
r35127:ec25c827 default
parent child Browse files
Show More
@@ -1,319 +1,300 b''
1 1 # changelog bisection for mercurial
2 2 #
3 3 # Copyright 2007 Matt Mackall
4 4 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
5 5 #
6 6 # Inspired by git bisect, extension skeleton taken from mq.py.
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import collections
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 short,
19 19 )
20 20 from . import (
21 21 error,
22 22 )
23 23
24 24 def bisect(repo, state):
25 25 """find the next node (if any) for testing during a bisect search.
26 26 returns a (nodes, number, good) tuple.
27 27
28 28 'nodes' is the final result of the bisect if 'number' is 0.
29 29 Otherwise 'number' indicates the remaining possible candidates for
30 30 the search and 'nodes' contains the next bisect target.
31 31 'good' is True if bisect is searching for a first good changeset, False
32 32 if searching for a first bad one.
33 33 """
34 34
35 35 changelog = repo.changelog
36 36 clparents = changelog.parentrevs
37 37 skip = set([changelog.rev(n) for n in state['skip']])
38 38
39 39 def buildancestors(bad, good):
40 # only the earliest bad revision matters
41 40 badrev = min([changelog.rev(n) for n in bad])
42 goodrevs = [changelog.rev(n) for n in good]
43 goodrev = min(goodrevs)
44 # build visit array
45 ancestors = [None] * (len(changelog) + 1) # an extra for [-1]
46
47 # set nodes descended from goodrevs
48 for rev in goodrevs:
41 ancestors = [None] * (len(changelog) + 1)
42 for rev in repo.revs("descendants(%ln) - ancestors(%ln)", good, good):
49 43 ancestors[rev] = []
50 for rev in changelog.revs(goodrev + 1):
51 for prev in clparents(rev):
52 if ancestors[prev] == []:
53 ancestors[rev] = []
54
55 # clear good revs from array
56 for rev in goodrevs:
57 ancestors[rev] = None
58 for rev in changelog.revs(len(changelog), goodrev):
59 if ancestors[rev] is None:
60 for prev in clparents(rev):
61 ancestors[prev] = None
62
63 44 if ancestors[badrev] is None:
64 45 return badrev, None
65 46 return badrev, ancestors
66 47
67 48 good = False
68 49 badrev, ancestors = buildancestors(state['bad'], state['good'])
69 50 if not ancestors: # looking for bad to good transition?
70 51 good = True
71 52 badrev, ancestors = buildancestors(state['good'], state['bad'])
72 53 bad = changelog.node(badrev)
73 54 if not ancestors: # now we're confused
74 55 if (len(state['bad']) == 1 and len(state['good']) == 1 and
75 56 state['bad'] != state['good']):
76 57 raise error.Abort(_("starting revisions are not directly related"))
77 58 raise error.Abort(_("inconsistent state, %s:%s is good and bad")
78 59 % (badrev, short(bad)))
79 60
80 61 # build children dict
81 62 children = {}
82 63 visit = collections.deque([badrev])
83 64 candidates = []
84 65 while visit:
85 66 rev = visit.popleft()
86 67 if ancestors[rev] == []:
87 68 candidates.append(rev)
88 69 for prev in clparents(rev):
89 70 if prev != -1:
90 71 if prev in children:
91 72 children[prev].append(rev)
92 73 else:
93 74 children[prev] = [rev]
94 75 visit.append(prev)
95 76
96 77 candidates.sort()
97 78 # have we narrowed it down to one entry?
98 79 # or have all other possible candidates besides 'bad' have been skipped?
99 80 tot = len(candidates)
100 81 unskipped = [c for c in candidates if (c not in skip) and (c != badrev)]
101 82 if tot == 1 or not unskipped:
102 83 return ([changelog.node(c) for c in candidates], 0, good)
103 84 perfect = tot // 2
104 85
105 86 # find the best node to test
106 87 best_rev = None
107 88 best_len = -1
108 89 poison = set()
109 90 for rev in candidates:
110 91 if rev in poison:
111 92 # poison children
112 93 poison.update(children.get(rev, []))
113 94 continue
114 95
115 96 a = ancestors[rev] or [rev]
116 97 ancestors[rev] = None
117 98
118 99 x = len(a) # number of ancestors
119 100 y = tot - x # number of non-ancestors
120 101 value = min(x, y) # how good is this test?
121 102 if value > best_len and rev not in skip:
122 103 best_len = value
123 104 best_rev = rev
124 105 if value == perfect: # found a perfect candidate? quit early
125 106 break
126 107
127 108 if y < perfect and rev not in skip: # all downhill from here?
128 109 # poison children
129 110 poison.update(children.get(rev, []))
130 111 continue
131 112
132 113 for c in children.get(rev, []):
133 114 if ancestors[c]:
134 115 ancestors[c] = list(set(ancestors[c] + a))
135 116 else:
136 117 ancestors[c] = a + [c]
137 118
138 119 assert best_rev is not None
139 120 best_node = changelog.node(best_rev)
140 121
141 122 return ([best_node], tot, good)
142 123
143 124 def extendrange(repo, state, nodes, good):
144 125 # bisect is incomplete when it ends on a merge node and
145 126 # one of the parent was not checked.
146 127 parents = repo[nodes[0]].parents()
147 128 if len(parents) > 1:
148 129 if good:
149 130 side = state['bad']
150 131 else:
151 132 side = state['good']
152 133 num = len(set(i.node() for i in parents) & set(side))
153 134 if num == 1:
154 135 return parents[0].ancestor(parents[1])
155 136 return None
156 137
157 138 def load_state(repo):
158 139 state = {'current': [], 'good': [], 'bad': [], 'skip': []}
159 140 for l in repo.vfs.tryreadlines("bisect.state"):
160 141 kind, node = l[:-1].split()
161 142 node = repo.lookup(node)
162 143 if kind not in state:
163 144 raise error.Abort(_("unknown bisect kind %s") % kind)
164 145 state[kind].append(node)
165 146 return state
166 147
167 148
168 149 def save_state(repo, state):
169 150 f = repo.vfs("bisect.state", "w", atomictemp=True)
170 151 with repo.wlock():
171 152 for kind in sorted(state):
172 153 for node in state[kind]:
173 154 f.write("%s %s\n" % (kind, hex(node)))
174 155 f.close()
175 156
176 157 def resetstate(repo):
177 158 """remove any bisect state from the repository"""
178 159 if repo.vfs.exists("bisect.state"):
179 160 repo.vfs.unlink("bisect.state")
180 161
181 162 def checkstate(state):
182 163 """check we have both 'good' and 'bad' to define a range
183 164
184 165 Raise Abort exception otherwise."""
185 166 if state['good'] and state['bad']:
186 167 return True
187 168 if not state['good']:
188 169 raise error.Abort(_('cannot bisect (no known good revisions)'))
189 170 else:
190 171 raise error.Abort(_('cannot bisect (no known bad revisions)'))
191 172
192 173 def get(repo, status):
193 174 """
194 175 Return a list of revision(s) that match the given status:
195 176
196 177 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
197 178 - ``goods``, ``bads`` : csets topologically good/bad
198 179 - ``range`` : csets taking part in the bisection
199 180 - ``pruned`` : csets that are goods, bads or skipped
200 181 - ``untested`` : csets whose fate is yet unknown
201 182 - ``ignored`` : csets ignored due to DAG topology
202 183 - ``current`` : the cset currently being bisected
203 184 """
204 185 state = load_state(repo)
205 186 if status in ('good', 'bad', 'skip', 'current'):
206 187 return map(repo.changelog.rev, state[status])
207 188 else:
208 189 # In the following sets, we do *not* call 'bisect()' with more
209 190 # than one level of recursion, because that can be very, very
210 191 # time consuming. Instead, we always develop the expression as
211 192 # much as possible.
212 193
213 194 # 'range' is all csets that make the bisection:
214 195 # - have a good ancestor and a bad descendant, or conversely
215 196 # that's because the bisection can go either way
216 197 range = '( bisect(bad)::bisect(good) | bisect(good)::bisect(bad) )'
217 198
218 199 _t = repo.revs('bisect(good)::bisect(bad)')
219 200 # The sets of topologically good or bad csets
220 201 if len(_t) == 0:
221 202 # Goods are topologically after bads
222 203 goods = 'bisect(good)::' # Pruned good csets
223 204 bads = '::bisect(bad)' # Pruned bad csets
224 205 else:
225 206 # Goods are topologically before bads
226 207 goods = '::bisect(good)' # Pruned good csets
227 208 bads = 'bisect(bad)::' # Pruned bad csets
228 209
229 210 # 'pruned' is all csets whose fate is already known: good, bad, skip
230 211 skips = 'bisect(skip)' # Pruned skipped csets
231 212 pruned = '( (%s) | (%s) | (%s) )' % (goods, bads, skips)
232 213
233 214 # 'untested' is all cset that are- in 'range', but not in 'pruned'
234 215 untested = '( (%s) - (%s) )' % (range, pruned)
235 216
236 217 # 'ignored' is all csets that were not used during the bisection
237 218 # due to DAG topology, but may however have had an impact.
238 219 # E.g., a branch merged between bads and goods, but whose branch-
239 220 # point is out-side of the range.
240 221 iba = '::bisect(bad) - ::bisect(good)' # Ignored bads' ancestors
241 222 iga = '::bisect(good) - ::bisect(bad)' # Ignored goods' ancestors
242 223 ignored = '( ( (%s) | (%s) ) - (%s) )' % (iba, iga, range)
243 224
244 225 if status == 'range':
245 226 return repo.revs(range)
246 227 elif status == 'pruned':
247 228 return repo.revs(pruned)
248 229 elif status == 'untested':
249 230 return repo.revs(untested)
250 231 elif status == 'ignored':
251 232 return repo.revs(ignored)
252 233 elif status == "goods":
253 234 return repo.revs(goods)
254 235 elif status == "bads":
255 236 return repo.revs(bads)
256 237 else:
257 238 raise error.ParseError(_('invalid bisect state'))
258 239
259 240 def label(repo, node):
260 241 rev = repo.changelog.rev(node)
261 242
262 243 # Try explicit sets
263 244 if rev in get(repo, 'good'):
264 245 # i18n: bisect changeset status
265 246 return _('good')
266 247 if rev in get(repo, 'bad'):
267 248 # i18n: bisect changeset status
268 249 return _('bad')
269 250 if rev in get(repo, 'skip'):
270 251 # i18n: bisect changeset status
271 252 return _('skipped')
272 253 if rev in get(repo, 'untested') or rev in get(repo, 'current'):
273 254 # i18n: bisect changeset status
274 255 return _('untested')
275 256 if rev in get(repo, 'ignored'):
276 257 # i18n: bisect changeset status
277 258 return _('ignored')
278 259
279 260 # Try implicit sets
280 261 if rev in get(repo, 'goods'):
281 262 # i18n: bisect changeset status
282 263 return _('good (implicit)')
283 264 if rev in get(repo, 'bads'):
284 265 # i18n: bisect changeset status
285 266 return _('bad (implicit)')
286 267
287 268 return None
288 269
289 270 def shortlabel(label):
290 271 if label:
291 272 return label[0].upper()
292 273
293 274 return None
294 275
295 276 def printresult(ui, repo, state, displayer, nodes, good):
296 277 if len(nodes) == 1:
297 278 # narrowed it down to a single revision
298 279 if good:
299 280 ui.write(_("The first good revision is:\n"))
300 281 else:
301 282 ui.write(_("The first bad revision is:\n"))
302 283 displayer.show(repo[nodes[0]])
303 284 extendnode = extendrange(repo, state, nodes, good)
304 285 if extendnode is not None:
305 286 ui.write(_('Not all ancestors of this changeset have been'
306 287 ' checked.\nUse bisect --extend to continue the '
307 288 'bisection from\nthe common ancestor, %s.\n')
308 289 % extendnode)
309 290 else:
310 291 # multiple possible revisions
311 292 if good:
312 293 ui.write(_("Due to skipped revisions, the first "
313 294 "good revision could be any of:\n"))
314 295 else:
315 296 ui.write(_("Due to skipped revisions, the first "
316 297 "bad revision could be any of:\n"))
317 298 for n in nodes:
318 299 displayer.show(repo[n])
319 300 displayer.close()
General Comments 0
You need to be logged in to leave comments. Login now