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