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