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