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