Show More
@@ -1,213 +1,203 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, array |
|
11 | import os, array | |
12 |
|
12 | |||
13 | class bisect(object): |
|
13 | class bisect(object): | |
14 | """dichotomic search in the DAG of changesets""" |
|
14 | """dichotomic search in the DAG of changesets""" | |
15 | def __init__(self, ui, repo): |
|
15 | def __init__(self, ui, repo): | |
16 | self.repo = repo |
|
16 | self.repo = repo | |
17 | self.ui = ui |
|
17 | self.ui = ui | |
18 | self.goodnodes = [] |
|
18 | self.goodnodes = [] | |
19 | self.skipnodes = [] |
|
19 | self.skipnodes = [] | |
20 | self.badnode = None |
|
20 | self.badnode = None | |
21 |
|
21 | |||
22 | p = self.repo.join("bisect.state") |
|
22 | p = self.repo.join("bisect.state") | |
23 | if os.path.exists(p): |
|
23 | if os.path.exists(p): | |
24 | for l in self.repo.opener("bisect.state"): |
|
24 | for l in self.repo.opener("bisect.state"): | |
25 | type, node = l[:-1].split() |
|
25 | type, node = l[:-1].split() | |
26 | node = self.repo.lookup(node) |
|
26 | node = self.repo.lookup(node) | |
27 | if type == "good": |
|
27 | if type == "good": | |
28 | self.goodnodes.append(node) |
|
28 | self.goodnodes.append(node) | |
29 | elif type == "skip": |
|
29 | elif type == "skip": | |
30 | self.skipnodes.append(node) |
|
30 | self.skipnodes.append(node) | |
31 | elif type == "bad": |
|
31 | elif type == "bad": | |
32 | self.badnode = node |
|
32 | self.badnode = node | |
33 |
|
33 | |||
34 | def write(self): |
|
34 | def write(self): | |
35 | f = self.repo.opener("bisect.state", "w") |
|
35 | f = self.repo.opener("bisect.state", "w") | |
36 | for n in self.goodnodes: |
|
36 | for n in self.goodnodes: | |
37 | f.write("good %s\n" % hg.hex(n)) |
|
37 | f.write("good %s\n" % hg.hex(n)) | |
38 | for n in self.skipnodes: |
|
38 | for n in self.skipnodes: | |
39 | f.write("skip %s\n" % hg.hex(n)) |
|
39 | f.write("skip %s\n" % hg.hex(n)) | |
40 | if self.badnode: |
|
40 | if self.badnode: | |
41 | f.write("bad %s\n" % hg.hex(self.badnode)) |
|
41 | f.write("bad %s\n" % hg.hex(self.badnode)) | |
42 |
|
42 | |||
43 | def init(self): |
|
43 | def init(self): | |
44 | """start a new bisection""" |
|
44 | """start a new bisection""" | |
45 | p = self.repo.join("bisect.state") |
|
45 | p = self.repo.join("bisect.state") | |
46 | if os.path.exists(p): |
|
46 | if os.path.exists(p): | |
47 | os.unlink(p) |
|
47 | os.unlink(p) | |
48 |
|
48 | |||
49 | def bisect(self): |
|
49 | def bisect(self): | |
50 | cl = self.repo.changelog |
|
50 | cl = self.repo.changelog | |
51 | clparents = cl.parentrevs |
|
51 | clparents = cl.parentrevs | |
52 | bad = self.badnode |
|
52 | bad = self.badnode | |
53 | badrev = cl.rev(bad) |
|
53 | badrev = cl.rev(bad) | |
54 |
|
54 | |||
55 | # build ancestors array |
|
55 | # build ancestors array | |
56 | ancestors = [[]] * (cl.count() + 1) # an extra for [-1] |
|
56 | ancestors = [[]] * (cl.count() + 1) # an extra for [-1] | |
57 |
|
57 | |||
58 | # clear goodnodes from array |
|
58 | # clear goodnodes from array | |
59 | for good in self.goodnodes: |
|
59 | for good in self.goodnodes: | |
60 | ancestors[cl.rev(good)] = None |
|
60 | ancestors[cl.rev(good)] = None | |
61 | for rev in xrange(cl.count(), -1, -1): |
|
61 | for rev in xrange(cl.count(), -1, -1): | |
62 | if ancestors[rev] is None: |
|
62 | if ancestors[rev] is None: | |
63 | for prev in clparents(rev): |
|
63 | for prev in clparents(rev): | |
64 | ancestors[prev] = None |
|
64 | ancestors[prev] = None | |
65 |
|
65 | |||
66 | if ancestors[badrev] is None: |
|
66 | if ancestors[badrev] is None: | |
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, hg.short(bad))) |
|
68 | % (badrev, hg.short(bad))) | |
69 |
|
69 | |||
70 | # accumulate ancestor lists |
|
70 | # accumulate ancestor lists | |
71 | for rev in xrange(badrev + 1): |
|
71 | for rev in xrange(badrev + 1): | |
72 | if ancestors[rev] == []: |
|
72 | if ancestors[rev] == []: | |
73 | p1, p2 = clparents(rev) |
|
73 | p1, p2 = clparents(rev) | |
74 | a1, a2 = ancestors[p1], ancestors[p2] |
|
74 | a1, a2 = ancestors[p1], ancestors[p2] | |
75 | if a1: |
|
75 | if a1: | |
76 | if a2: |
|
76 | if a2: | |
77 | # merge ancestor lists |
|
77 | # merge ancestor lists | |
78 | a = dict.fromkeys(a2) |
|
78 | a = dict.fromkeys(a2) | |
79 | a.update(dict.fromkeys(a1)) |
|
79 | a.update(dict.fromkeys(a1)) | |
80 | a[rev] = None |
|
80 | a[rev] = None | |
81 | ancestors[rev] = array.array("l", a.keys()) |
|
81 | ancestors[rev] = array.array("l", a.keys()) | |
82 | else: |
|
82 | else: | |
83 | ancestors[rev] = a1 + array.array("l", [rev]) |
|
83 | ancestors[rev] = a1 + array.array("l", [rev]) | |
84 | elif a2: |
|
84 | elif a2: | |
85 | ancestors[rev] = a2 + array.array("l", [rev]) |
|
85 | ancestors[rev] = a2 + array.array("l", [rev]) | |
86 | else: |
|
86 | else: | |
87 | ancestors[rev] = array.array("l", [rev]) |
|
87 | ancestors[rev] = array.array("l", [rev]) | |
88 |
|
88 | |||
89 | if badrev not in ancestors[badrev]: |
|
89 | if badrev not in ancestors[badrev]: | |
90 | raise util.Abort(_("Could not find the first bad revision")) |
|
90 | raise util.Abort(_("Could not find the first bad revision")) | |
91 |
|
91 | |||
92 | # have we narrowed it down to one entry? |
|
92 | # have we narrowed it down to one entry? | |
93 | tot = len(ancestors[badrev]) |
|
93 | tot = len(ancestors[badrev]) | |
94 | if tot == 1: |
|
94 | if tot == 1: | |
95 | return (self.badnode, 0) |
|
95 | return (self.badnode, 0) | |
96 |
|
96 | |||
97 | # find the best node to test |
|
97 | # find the best node to test | |
98 | best_rev = None |
|
98 | best_rev = None | |
99 | best_len = -1 |
|
99 | best_len = -1 | |
100 | skip = dict.fromkeys([cl.rev(s) for s in self.skipnodes]) |
|
100 | skip = dict.fromkeys([cl.rev(s) for s in self.skipnodes]) | |
101 | for n in ancestors[badrev]: |
|
101 | for n in ancestors[badrev]: | |
102 | if n in skip: |
|
102 | if n in skip: | |
103 | continue |
|
103 | continue | |
104 | a = len(ancestors[n]) # number of ancestors |
|
104 | a = len(ancestors[n]) # number of ancestors | |
105 | b = tot - a # number of non-ancestors |
|
105 | b = tot - a # number of non-ancestors | |
106 | value = min(a, b) # how good is this test? |
|
106 | value = min(a, b) # how good is this test? | |
107 | if value > best_len: |
|
107 | if value > best_len: | |
108 | best_len = value |
|
108 | best_len = value | |
109 | best_rev = n |
|
109 | best_rev = n | |
110 | assert best_rev is not None |
|
110 | assert best_rev is not None | |
111 | best_node = cl.node(best_rev) |
|
111 | best_node = cl.node(best_rev) | |
112 |
|
112 | |||
113 | return (best_node, tot) |
|
113 | return (best_node, tot) | |
114 |
|
114 | |||
115 | def next(self): |
|
115 | def next(self): | |
116 | """find and update to the next revision to test""" |
|
116 | """find and update to the next revision to test""" | |
117 | if self.goodnodes and self.badnode: |
|
117 | if self.goodnodes and self.badnode: | |
118 | node, changesets = self.bisect() |
|
118 | node, changesets = self.bisect() | |
119 |
|
119 | |||
120 | if changesets == 0: |
|
120 | if changesets == 0: | |
121 | self.ui.write(_("The first bad revision is:\n")) |
|
121 | self.ui.write(_("The first bad revision is:\n")) | |
122 | displayer = cmdutil.show_changeset(self.ui, self.repo, {}) |
|
122 | displayer = cmdutil.show_changeset(self.ui, self.repo, {}) | |
123 | displayer.show(changenode=node) |
|
123 | displayer.show(changenode=node) | |
124 | elif node is not None: |
|
124 | elif node is not None: | |
125 | # compute the approximate number of remaining tests |
|
125 | # compute the approximate number of remaining tests | |
126 | tests, size = 0, 2 |
|
126 | tests, size = 0, 2 | |
127 | while size <= changesets: |
|
127 | while size <= changesets: | |
128 | tests, size = tests + 1, size * 2 |
|
128 | tests, size = tests + 1, size * 2 | |
129 | rev = self.repo.changelog.rev(node) |
|
129 | rev = self.repo.changelog.rev(node) | |
130 | self.ui.write(_("Testing changeset %s:%s " |
|
130 | self.ui.write(_("Testing changeset %s:%s " | |
131 | "(%s changesets remaining, ~%s tests)\n") |
|
131 | "(%s changesets remaining, ~%s tests)\n") | |
132 | % (rev, hg.short(node), changesets, tests)) |
|
132 | % (rev, hg.short(node), changesets, tests)) | |
133 | cmdutil.bail_if_changed(self.repo) |
|
133 | cmdutil.bail_if_changed(self.repo) | |
134 | return hg.clean(self.repo, node) |
|
134 | return hg.clean(self.repo, node) | |
135 |
|
135 | |||
136 | def good(self, rev=None): |
|
136 | def good(self, rev=None): | |
137 | """mark revision as good and update to the next revision to test""" |
|
137 | """mark revision as good and update to the next revision to test""" | |
138 | self.goodnodes.append(self.repo.lookup(rev or '.')) |
|
138 | self.goodnodes.append(self.repo.lookup(rev or '.')) | |
139 | self.write() |
|
139 | self.write() | |
140 | return self.next() |
|
140 | return self.next() | |
141 |
|
141 | |||
142 | def skip(self, rev=None): |
|
142 | def skip(self, rev=None): | |
143 | """mark revision as skipped and update to the next revision to test""" |
|
143 | """mark revision as skipped and update to the next revision to test""" | |
144 | self.skipnodes.append(self.repo.lookup(rev or '.')) |
|
144 | self.skipnodes.append(self.repo.lookup(rev or '.')) | |
145 | self.write() |
|
145 | self.write() | |
146 | return self.next() |
|
146 | return self.next() | |
147 |
|
147 | |||
148 | def bad(self, rev=None): |
|
148 | def bad(self, rev=None): | |
149 | """mark revision as bad and update to the next revision to test""" |
|
149 | """mark revision as bad and update to the next revision to test""" | |
150 | self.badnode = self.repo.lookup(rev or '.') |
|
150 | self.badnode = self.repo.lookup(rev or '.') | |
151 | self.write() |
|
151 | self.write() | |
152 | return self.next() |
|
152 | return self.next() | |
153 |
|
153 | |||
154 |
def bisect_run(ui, repo, |
|
154 | def bisect_run(ui, repo, node=None, extra=None, | |
|
155 | reset=None, good=None, bad=None, skip=None): | |||
155 | """Subdivision search of changesets |
|
156 | """Subdivision search of changesets | |
156 |
|
157 | |||
157 | This extension helps to find changesets which introduce problems. |
|
158 | This extension helps to find changesets which introduce problems. | |
158 | To use, mark the earliest changeset you know exhibits the problem |
|
159 | To use, mark the earliest changeset you know exhibits the problem | |
159 | as bad, then mark the latest changeset which is free from the problem |
|
160 | as bad, then mark the latest changeset which is free from the problem | |
160 | as good. Bisect will update your working directory to a revision for |
|
161 | as good. Bisect will update your working directory to a revision for | |
161 | testing. Once you have performed tests, mark the working directory |
|
162 | testing. Once you have performed tests, mark the working directory | |
162 | as bad or good and bisect will either update to another candidate |
|
163 | as bad or good and bisect will either update to another candidate | |
163 | changeset or announce that it has found the bad revision. |
|
164 | changeset or announce that it has found the bad revision. | |
164 |
|
165 | |||
165 | Note: bisect expects bad revisions to be descendants of good revisions. |
|
166 | Note: bisect expects bad revisions to be descendants of good revisions. | |
166 | If you are looking for the point at which a problem was fixed, then make |
|
167 | If you are looking for the point at which a problem was fixed, then make | |
167 | the problem-free state "bad" and the problematic state "good." |
|
168 | the problem-free state "bad" and the problematic state "good." | |
168 |
|
169 | |||
169 | For subcommands see "hg bisect help\" |
|
|||
170 | """ |
|
170 | """ | |
171 | def help_(cmd=None, *args): |
|
171 | # backward compatibility | |
172 | """show help for a given bisect subcommand or all subcommands""" |
|
172 | if node in "good bad reset init".split(): | |
173 | cmdtable = bisectcmdtable |
|
173 | ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n")) | |
174 | if cmd: |
|
174 | cmd, node, extra = node, extra, None | |
175 | doc = cmdtable[cmd][0].__doc__ |
|
175 | if cmd == "good": | |
176 | synopsis = cmdtable[cmd][2] |
|
176 | good = True | |
177 | ui.write(synopsis + "\n") |
|
177 | elif cmd == "bad": | |
178 | ui.write("\n" + doc + "\n") |
|
178 | bad = True | |
179 | return |
|
179 | else: | |
180 | ui.write(_("list of subcommands for the bisect extension\n\n")) |
|
180 | reset = True | |
181 | cmds = cmdtable.keys() |
|
181 | elif extra or good + bad + skip + reset > 1: | |
182 | cmds.sort() |
|
182 | raise util.Abort("Incompatible arguments") | |
183 | m = max([len(c) for c in cmds]) |
|
|||
184 | for cmd in cmds: |
|
|||
185 | doc = cmdtable[cmd][0].__doc__.splitlines(0)[0].rstrip() |
|
|||
186 | ui.write(" %-*s %s\n" % (m, cmd, doc)) |
|
|||
187 |
|
183 | |||
188 | b = bisect(ui, repo) |
|
184 | b = bisect(ui, repo) | |
189 | bisectcmdtable = { |
|
185 | if good: | |
190 | "init": (b.init, 0, _("hg bisect init")), |
|
186 | return b.good(node) | |
191 | "bad": (b.bad, 1, _("hg bisect bad [<rev>]")), |
|
187 | elif bad: | |
192 | "good": (b.good, 1, _("hg bisect good [<rev>]")), |
|
188 | return b.bad(node) | |
193 | "skip": (b.skip, 1, _("hg bisect skip [<rev>]")), |
|
189 | elif skip: | |
194 | "next": (b.next, 0, _("hg bisect next")), |
|
190 | return b.skip(node) | |
195 | "help": (help_, 1, _("hg bisect help [<subcommand>]")), |
|
191 | elif reset: | |
196 | } |
|
192 | return b.init() | |
197 |
|
193 | else: | ||
198 | if cmd == "reset": |
|
194 | return b.next() | |
199 | cmd = "init" |
|
|||
200 |
|
||||
201 | if not bisectcmdtable.has_key(cmd): |
|
|||
202 | ui.warn(_("bisect: Unknown sub-command\n")) |
|
|||
203 | return help_() |
|
|||
204 | if len(args) > bisectcmdtable[cmd][1]: |
|
|||
205 | ui.warn(_("bisect: Too many arguments\n")) |
|
|||
206 | return help_() |
|
|||
207 | ret = bisectcmdtable[cmd][0](*args) |
|
|||
208 | return ret |
|
|||
209 |
|
195 | |||
210 | cmdtable = { |
|
196 | cmdtable = { | |
211 | "bisect": (bisect_run, [], _("hg bisect [help|init|reset|next|good|bad]")), |
|
197 | "bisect": (bisect_run, | |
212 | #"bisect-test": (test, [], "hg bisect-test rev"), |
|
198 | [('r', 'reset', False, _('reset bisect state')), | |
|
199 | ('g', 'good', False, _('mark changeset good')), | |||
|
200 | ('b', 'bad', False, _('mark changeset bad')), | |||
|
201 | ('s', 'skip', False, _('skip testing changeset'))], | |||
|
202 | _("hg bisect [-gbsr] [REV]")) | |||
213 | } |
|
203 | } |
@@ -1,36 +1,36 b'' | |||||
1 | #!/bin/sh |
|
1 | #!/bin/sh | |
2 |
|
2 | |||
3 | set -e |
|
3 | set -e | |
4 |
|
4 | |||
5 | echo "[extensions]" >> $HGRCPATH |
|
5 | echo "[extensions]" >> $HGRCPATH | |
6 | echo "hbisect=" >> $HGRCPATH |
|
6 | echo "hbisect=" >> $HGRCPATH | |
7 |
|
7 | |||
8 | echo % init |
|
8 | echo % init | |
9 | hg init |
|
9 | hg init | |
10 |
|
10 | |||
11 | echo % committing changes |
|
11 | echo % committing changes | |
12 | count=0 |
|
12 | count=0 | |
13 | echo > a |
|
13 | echo > a | |
14 | while test $count -lt 32 ; do |
|
14 | while test $count -lt 32 ; do | |
15 | echo 'a' >> a |
|
15 | echo 'a' >> a | |
16 | test $count -eq 0 && hg add |
|
16 | test $count -eq 0 && hg add | |
17 | hg ci -m "msg $count" -d "$count 0" |
|
17 | hg ci -m "msg $count" -d "$count 0" | |
18 | echo % committed changeset $count |
|
18 | echo % committed changeset $count | |
19 | count=`expr $count + 1` |
|
19 | count=`expr $count + 1` | |
20 | done |
|
20 | done | |
21 |
|
21 | |||
22 | echo % log |
|
22 | echo % log | |
23 | hg log |
|
23 | hg log | |
24 |
|
24 | |||
25 | echo % hg up -C |
|
25 | echo % hg up -C | |
26 | hg up -C |
|
26 | hg up -C | |
27 |
|
27 | |||
28 | echo % bisect test |
|
28 | echo % bisect test | |
29 |
hg bisect |
|
29 | hg bisect -r | |
30 |
hg bisect b |
|
30 | hg bisect -b | |
31 |
hg bisect g |
|
31 | hg bisect -g 1 | |
32 |
hg bisect g |
|
32 | hg bisect -g | |
33 |
hg bisect g |
|
33 | hg bisect -g | |
34 |
hg bisect g |
|
34 | hg bisect -g | |
35 |
hg bisect b |
|
35 | hg bisect -b | |
36 |
hg bisect g |
|
36 | hg bisect -g |
General Comments 0
You need to be logged in to leave comments.
Login now