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