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