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