##// END OF EJS Templates
bisect: faster merging
Matt Mackall -
r5774:c850a864 default
parent child Browse files
Show More
@@ -1,194 +1,192 b''
1 # bisect extension for mercurial
1 # bisect extension for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
3 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
4 # Inspired by git bisect, extension skeleton taken from mq.py.
4 # Inspired by git bisect, extension skeleton taken from mq.py.
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial import hg, util, cmdutil
10 from mercurial import hg, util, cmdutil
11 import os
11 import os
12
12
13 def _bisect(changelog, state):
13 def _bisect(changelog, state):
14 clparents = changelog.parentrevs
14 clparents = changelog.parentrevs
15 # only the earliest bad revision matters
15 # only the earliest bad revision matters
16 badrev = min([changelog.rev(n) for n in state['bad']])
16 badrev = min([changelog.rev(n) for n in state['bad']])
17 bad = changelog.node(badrev)
17 bad = changelog.node(badrev)
18 skip = dict.fromkeys([changelog.rev(n) for n in state['skip']])
18 skip = dict.fromkeys([changelog.rev(n) for n in state['skip']])
19
19
20 # build ancestors array
20 # build ancestors array
21 ancestors = [[]] * (changelog.count() + 1) # an extra for [-1]
21 ancestors = [[]] * (changelog.count() + 1) # an extra for [-1]
22
22
23 # clear good revs from array
23 # clear good revs from array
24 for node in state['good']:
24 for node in state['good']:
25 ancestors[changelog.rev(node)] = None
25 ancestors[changelog.rev(node)] = None
26 for rev in xrange(changelog.count(), -1, -1):
26 for rev in xrange(changelog.count(), -1, -1):
27 if ancestors[rev] is None:
27 if ancestors[rev] is None:
28 for prev in clparents(rev):
28 for prev in clparents(rev):
29 ancestors[prev] = None
29 ancestors[prev] = None
30
30
31 if ancestors[badrev] is None:
31 if ancestors[badrev] is None:
32 raise util.Abort(_("Inconsistent state, %s:%s is good and bad")
32 raise util.Abort(_("Inconsistent state, %s:%s is good and bad")
33 % (badrev, hg.short(bad)))
33 % (badrev, hg.short(bad)))
34
34
35 # build children dict
35 # build children dict
36 children = {}
36 children = {}
37 visit = [badrev]
37 visit = [badrev]
38 candidates = []
38 candidates = []
39 while visit:
39 while visit:
40 rev = visit.pop(0)
40 rev = visit.pop(0)
41 if ancestors[rev] == []:
41 if ancestors[rev] == []:
42 candidates.append(rev)
42 candidates.append(rev)
43 for prev in clparents(rev):
43 for prev in clparents(rev):
44 if prev != -1:
44 if prev != -1:
45 if prev in children:
45 if prev in children:
46 children[prev].append(rev)
46 children[prev].append(rev)
47 else:
47 else:
48 children[prev] = [rev]
48 children[prev] = [rev]
49 visit.append(prev)
49 visit.append(prev)
50
50
51 candidates.sort()
51 candidates.sort()
52 # have we narrowed it down to one entry?
52 # have we narrowed it down to one entry?
53 tot = len(candidates)
53 tot = len(candidates)
54 if tot == 1:
54 if tot == 1:
55 return (bad, 0)
55 return (bad, 0)
56 perfect = tot / 2
56 perfect = tot / 2
57
57
58 # find the best node to test
58 # find the best node to test
59 best_rev = None
59 best_rev = None
60 best_len = -1
60 best_len = -1
61 poison = {}
61 poison = {}
62 for rev in candidates:
62 for rev in candidates:
63 if rev in poison:
63 if rev in poison:
64 for c in children.get(rev, []):
64 for c in children.get(rev, []):
65 poison[c] = True # poison children
65 poison[c] = True # poison children
66 continue
66 continue
67
67
68 a = ancestors[rev] or [rev]
68 a = ancestors[rev] or [rev]
69 ancestors[rev] = None
69 ancestors[rev] = None
70
70
71 x = len(a) # number of ancestors
71 x = len(a) # number of ancestors
72 y = tot - x # number of non-ancestors
72 y = tot - x # number of non-ancestors
73 value = min(x, y) # how good is this test?
73 value = min(x, y) # how good is this test?
74 if value > best_len and rev not in skip:
74 if value > best_len and rev not in skip:
75 best_len = value
75 best_len = value
76 best_rev = rev
76 best_rev = rev
77 if value == perfect: # found a perfect candidate? quit early
77 if value == perfect: # found a perfect candidate? quit early
78 break
78 break
79
79
80 if y < perfect: # all downhill from here?
80 if y < perfect: # all downhill from here?
81 for c in children.get(rev, []):
81 for c in children.get(rev, []):
82 poison[c] = True # poison children
82 poison[c] = True # poison children
83 continue
83 continue
84
84
85 for c in children.get(rev, []):
85 for c in children.get(rev, []):
86 if ancestors[c]:
86 if ancestors[c]:
87 s = dict.fromkeys(ancestors[c])
87 ancestors[c] = dict.fromkeys(ancestors[c] + a).keys()
88 s.update(dict.fromkeys(a))
89 ancestors[c] = s.keys()
90 else:
88 else:
91 ancestors[c] = a + [c]
89 ancestors[c] = a + [c]
92
90
93 assert best_rev is not None
91 assert best_rev is not None
94 best_node = changelog.node(best_rev)
92 best_node = changelog.node(best_rev)
95
93
96 return (best_node, tot)
94 return (best_node, tot)
97
95
98 def bisect(ui, repo, rev=None, extra=None,
96 def bisect(ui, repo, rev=None, extra=None,
99 reset=None, good=None, bad=None, skip=None, noupdate=None):
97 reset=None, good=None, bad=None, skip=None, noupdate=None):
100 """Subdivision search of changesets
98 """Subdivision search of changesets
101
99
102 This extension helps to find changesets which introduce problems.
100 This extension helps to find changesets which introduce problems.
103 To use, mark the earliest changeset you know exhibits the problem
101 To use, mark the earliest changeset you know exhibits the problem
104 as bad, then mark the latest changeset which is free from the problem
102 as bad, then mark the latest changeset which is free from the problem
105 as good. Bisect will update your working directory to a revision for
103 as good. Bisect will update your working directory to a revision for
106 testing. Once you have performed tests, mark the working directory
104 testing. Once you have performed tests, mark the working directory
107 as bad or good and bisect will either update to another candidate
105 as bad or good and bisect will either update to another candidate
108 changeset or announce that it has found the bad revision.
106 changeset or announce that it has found the bad revision.
109
107
110 Note: bisect expects bad revisions to be descendants of good revisions.
108 Note: bisect expects bad revisions to be descendants of good revisions.
111 If you are looking for the point at which a problem was fixed, then make
109 If you are looking for the point at which a problem was fixed, then make
112 the problem-free state "bad" and the problematic state "good."
110 the problem-free state "bad" and the problematic state "good."
113
111
114 """
112 """
115 # backward compatibility
113 # backward compatibility
116 if rev in "good bad reset init".split():
114 if rev in "good bad reset init".split():
117 ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
115 ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
118 cmd, rev, extra = rev, extra, None
116 cmd, rev, extra = rev, extra, None
119 if cmd == "good":
117 if cmd == "good":
120 good = True
118 good = True
121 elif cmd == "bad":
119 elif cmd == "bad":
122 bad = True
120 bad = True
123 else:
121 else:
124 reset = True
122 reset = True
125 elif extra or good + bad + skip + reset > 1:
123 elif extra or good + bad + skip + reset > 1:
126 raise util.Abort("Incompatible arguments")
124 raise util.Abort("Incompatible arguments")
127
125
128 if reset:
126 if reset:
129 p = repo.join("bisect.state")
127 p = repo.join("bisect.state")
130 if os.path.exists(p):
128 if os.path.exists(p):
131 os.unlink(p)
129 os.unlink(p)
132 return
130 return
133
131
134 # load state
132 # load state
135 state = {'good': [], 'bad': [], 'skip': []}
133 state = {'good': [], 'bad': [], 'skip': []}
136 if os.path.exists(repo.join("bisect.state")):
134 if os.path.exists(repo.join("bisect.state")):
137 for l in repo.opener("bisect.state"):
135 for l in repo.opener("bisect.state"):
138 kind, node = l[:-1].split()
136 kind, node = l[:-1].split()
139 node = repo.lookup(node)
137 node = repo.lookup(node)
140 if kind not in state:
138 if kind not in state:
141 raise util.Abort(_("unknown bisect kind %s") % kind)
139 raise util.Abort(_("unknown bisect kind %s") % kind)
142 state[kind].append(node)
140 state[kind].append(node)
143
141
144 # update state
142 # update state
145 node = repo.lookup(rev or '.')
143 node = repo.lookup(rev or '.')
146 if good:
144 if good:
147 state['good'].append(node)
145 state['good'].append(node)
148 elif bad:
146 elif bad:
149 state['bad'].append(node)
147 state['bad'].append(node)
150 elif skip:
148 elif skip:
151 state['skip'].append(node)
149 state['skip'].append(node)
152
150
153 # save state
151 # save state
154 f = repo.opener("bisect.state", "w", atomictemp=True)
152 f = repo.opener("bisect.state", "w", atomictemp=True)
155 wlock = repo.wlock()
153 wlock = repo.wlock()
156 try:
154 try:
157 for kind in state:
155 for kind in state:
158 for node in state[kind]:
156 for node in state[kind]:
159 f.write("%s %s\n" % (kind, hg.hex(node)))
157 f.write("%s %s\n" % (kind, hg.hex(node)))
160 f.rename()
158 f.rename()
161 finally:
159 finally:
162 del wlock
160 del wlock
163
161
164 if not state['good'] or not state['bad']:
162 if not state['good'] or not state['bad']:
165 return
163 return
166
164
167 # actually bisect
165 # actually bisect
168 node, changesets = _bisect(repo.changelog, state)
166 node, changesets = _bisect(repo.changelog, state)
169 if changesets == 0:
167 if changesets == 0:
170 ui.write(_("The first bad revision is:\n"))
168 ui.write(_("The first bad revision is:\n"))
171 displayer = cmdutil.show_changeset(ui, repo, {})
169 displayer = cmdutil.show_changeset(ui, repo, {})
172 displayer.show(changenode=node)
170 displayer.show(changenode=node)
173 elif node is not None:
171 elif node is not None:
174 # compute the approximate number of remaining tests
172 # compute the approximate number of remaining tests
175 tests, size = 0, 2
173 tests, size = 0, 2
176 while size <= changesets:
174 while size <= changesets:
177 tests, size = tests + 1, size * 2
175 tests, size = tests + 1, size * 2
178 rev = repo.changelog.rev(node)
176 rev = repo.changelog.rev(node)
179 ui.write(_("Testing changeset %s:%s "
177 ui.write(_("Testing changeset %s:%s "
180 "(%s changesets remaining, ~%s tests)\n")
178 "(%s changesets remaining, ~%s tests)\n")
181 % (rev, hg.short(node), changesets, tests))
179 % (rev, hg.short(node), changesets, tests))
182 if not noupdate:
180 if not noupdate:
183 cmdutil.bail_if_changed(repo)
181 cmdutil.bail_if_changed(repo)
184 return hg.clean(repo, node)
182 return hg.clean(repo, node)
185
183
186 cmdtable = {
184 cmdtable = {
187 "bisect": (bisect,
185 "bisect": (bisect,
188 [('r', 'reset', False, _('reset bisect state')),
186 [('r', 'reset', False, _('reset bisect state')),
189 ('g', 'good', False, _('mark changeset good')),
187 ('g', 'good', False, _('mark changeset good')),
190 ('b', 'bad', False, _('mark changeset bad')),
188 ('b', 'bad', False, _('mark changeset bad')),
191 ('s', 'skip', False, _('skip testing changeset')),
189 ('s', 'skip', False, _('skip testing changeset')),
192 ('U', 'noupdate', False, _('do not update to target'))],
190 ('U', 'noupdate', False, _('do not update to target'))],
193 _("hg bisect [-gbsr] [REV]"))
191 _("hg bisect [-gbsr] [REV]"))
194 }
192 }
General Comments 0
You need to be logged in to leave comments. Login now