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