##// END OF EJS Templates
Flesh out bisect help text
Brendan Cully -
r4390:052062b9 default
parent child Browse files
Show More
@@ -1,299 +1,312 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, commands, cmdutil
11 11 import os, sys, sets
12 12
13 13 versionstr = "0.0.3"
14 14
15 15 def lookup_rev(ui, repo, rev=None):
16 16 """returns rev or the checked-out revision if rev is None"""
17 17 if not rev is None:
18 18 return repo.lookup(rev)
19 19 parents = [p for p in repo.dirstate.parents() if p != hg.nullid]
20 20 if len(parents) != 1:
21 21 raise util.Abort(_("unexpected number of parents, "
22 22 "please commit or revert"))
23 23 return parents.pop()
24 24
25 25 def check_clean(ui, repo):
26 26 modified, added, removed, deleted, unknown = repo.status()[:5]
27 27 if modified or added or removed:
28 28 ui.warn("Repository is not clean, please commit or revert\n")
29 29 sys.exit(1)
30 30
31 31 class bisect(object):
32 32 """dichotomic search in the DAG of changesets"""
33 33 def __init__(self, ui, repo):
34 34 self.repo = repo
35 35 self.path = repo.join("bisect")
36 36 self.opener = util.opener(self.path)
37 37 self.ui = ui
38 38 self.goodrevs = []
39 39 self.badrev = None
40 40 self.good_dirty = 0
41 41 self.bad_dirty = 0
42 42 self.good_path = "good"
43 43 self.bad_path = "bad"
44 44
45 45 if os.path.exists(os.path.join(self.path, self.good_path)):
46 46 self.goodrevs = self.opener(self.good_path).read().splitlines()
47 47 self.goodrevs = [hg.bin(x) for x in self.goodrevs]
48 48 if os.path.exists(os.path.join(self.path, self.bad_path)):
49 49 r = self.opener(self.bad_path).read().splitlines()
50 50 if r:
51 51 self.badrev = hg.bin(r.pop(0))
52 52
53 53 def write(self):
54 54 if not os.path.isdir(self.path):
55 55 return
56 56 f = self.opener(self.good_path, "w")
57 57 f.write("\n".join([hg.hex(r) for r in self.goodrevs]))
58 58 if len(self.goodrevs) > 0:
59 59 f.write("\n")
60 60 f = self.opener(self.bad_path, "w")
61 61 if self.badrev:
62 62 f.write(hg.hex(self.badrev) + "\n")
63 63
64 64 def init(self):
65 65 """start a new bisection"""
66 66 if os.path.isdir(self.path):
67 67 raise util.Abort(_("bisect directory already exists\n"))
68 68 os.mkdir(self.path)
69 69 check_clean(self.ui, self.repo)
70 70 return 0
71 71
72 72 def reset(self):
73 73 """finish a bisection"""
74 74 if os.path.isdir(self.path):
75 75 sl = [os.path.join(self.path, p)
76 76 for p in [self.bad_path, self.good_path]]
77 77 for s in sl:
78 78 if os.path.exists(s):
79 79 os.unlink(s)
80 80 os.rmdir(self.path)
81 81 # Not sure about this
82 82 #self.ui.write("Going back to tip\n")
83 83 #self.repo.update(self.repo.changelog.tip())
84 84 return 1
85 85
86 86 def num_ancestors(self, head=None, stop=None):
87 87 """
88 88 returns a dict with the mapping:
89 89 node -> number of ancestors (self included)
90 90 for all nodes who are ancestor of head and
91 91 not in stop.
92 92 """
93 93 if head is None:
94 94 head = self.badrev
95 95 return self.__ancestors_and_nb_ancestors(head, stop)[1]
96 96
97 97 def ancestors(self, head=None, stop=None):
98 98 """
99 99 returns the set of the ancestors of head (self included)
100 100 who are not in stop.
101 101 """
102 102 if head is None:
103 103 head = self.badrev
104 104 return self.__ancestors_and_nb_ancestors(head, stop)[0]
105 105
106 106 def __ancestors_and_nb_ancestors(self, head, stop=None):
107 107 """
108 108 if stop is None then ancestors of goodrevs are used as
109 109 lower limit.
110 110
111 111 returns (anc, n_child) where anc is the set of the ancestors of head
112 112 and n_child is a dictionary with the following mapping:
113 113 node -> number of ancestors (self included)
114 114 """
115 115 cl = self.repo.changelog
116 116 if not stop:
117 117 stop = sets.Set([])
118 118 for i in xrange(len(self.goodrevs)-1, -1, -1):
119 119 g = self.goodrevs[i]
120 120 if g in stop:
121 121 continue
122 122 stop.update(cl.reachable(g))
123 123 def num_children(a):
124 124 """
125 125 returns a dictionnary with the following mapping
126 126 node -> [number of children, empty set]
127 127 """
128 128 d = {a: [0, sets.Set([])]}
129 129 for i in xrange(cl.rev(a)+1):
130 130 n = cl.node(i)
131 131 if not d.has_key(n):
132 132 d[n] = [0, sets.Set([])]
133 133 parents = [p for p in cl.parents(n) if p != hg.nullid]
134 134 for p in parents:
135 135 d[p][0] += 1
136 136 return d
137 137
138 138 if head in stop:
139 139 raise util.Abort(_("Unconsistent state, %s:%s is good and bad")
140 140 % (cl.rev(head), hg.short(head)))
141 141 n_child = num_children(head)
142 142 for i in xrange(cl.rev(head)+1):
143 143 n = cl.node(i)
144 144 parents = [p for p in cl.parents(n) if p != hg.nullid]
145 145 for p in parents:
146 146 n_child[p][0] -= 1
147 147 if not n in stop:
148 148 n_child[n][1].union_update(n_child[p][1])
149 149 if n_child[p][0] == 0:
150 150 n_child[p] = len(n_child[p][1])
151 151 if not n in stop:
152 152 n_child[n][1].add(n)
153 153 if n_child[n][0] == 0:
154 154 if n == head:
155 155 anc = n_child[n][1]
156 156 n_child[n] = len(n_child[n][1])
157 157 return anc, n_child
158 158
159 159 def next(self):
160 160 if not self.badrev:
161 161 raise util.Abort(_("You should give at least one bad revision"))
162 162 if not self.goodrevs:
163 163 self.ui.warn(_("No good revision given\n"))
164 164 self.ui.warn(_("Marking the first revision as good\n"))
165 165 ancestors, num_ancestors = self.__ancestors_and_nb_ancestors(
166 166 self.badrev)
167 167 tot = len(ancestors)
168 168 if tot == 1:
169 169 if ancestors.pop() != self.badrev:
170 170 raise util.Abort(_("Could not find the first bad revision"))
171 171 self.ui.write(_("The first bad revision is:\n"))
172 172 displayer = cmdutil.show_changeset(self.ui, self.repo, {})
173 173 displayer.show(changenode=self.badrev)
174 174 return None
175 175 best_rev = None
176 176 best_len = -1
177 177 for n in ancestors:
178 178 l = num_ancestors[n]
179 179 l = min(l, tot - l)
180 180 if l > best_len:
181 181 best_len = l
182 182 best_rev = n
183 183 assert best_rev is not None
184 184 nb_tests = 0
185 185 q, r = divmod(tot, 2)
186 186 while q:
187 187 nb_tests += 1
188 188 q, r = divmod(q, 2)
189 189 msg = _("Testing changeset %s:%s (%s changesets remaining, "
190 190 "~%s tests)\n") % (self.repo.changelog.rev(best_rev),
191 191 hg.short(best_rev), tot, nb_tests)
192 192 self.ui.write(msg)
193 193 return best_rev
194 194
195 195 def autonext(self):
196 196 """find and update to the next revision to test"""
197 197 check_clean(self.ui, self.repo)
198 198 rev = self.next()
199 199 if rev is not None:
200 200 return hg.clean(self.repo, rev)
201 201
202 202 def good(self, rev):
203 203 self.goodrevs.append(rev)
204 204
205 205 def autogood(self, rev=None):
206 206 """mark revision as good and update to the next revision to test"""
207 207 check_clean(self.ui, self.repo)
208 208 rev = lookup_rev(self.ui, self.repo, rev)
209 209 self.good(rev)
210 210 if self.badrev:
211 211 return self.autonext()
212 212
213 213 def bad(self, rev):
214 214 self.badrev = rev
215 215
216 216 def autobad(self, rev=None):
217 217 """mark revision as bad and update to the next revision to test"""
218 218 check_clean(self.ui, self.repo)
219 219 rev = lookup_rev(self.ui, self.repo, rev)
220 220 self.bad(rev)
221 221 if self.goodrevs:
222 222 self.autonext()
223 223
224 224 # should we put it in the class ?
225 225 def test(ui, repo, rev):
226 226 """test the bisection code"""
227 227 b = bisect(ui, repo)
228 228 rev = repo.lookup(rev)
229 229 ui.write("testing with rev %s\n" % hg.hex(rev))
230 230 anc = b.ancestors()
231 231 while len(anc) > 1:
232 232 if not rev in anc:
233 233 ui.warn("failure while bisecting\n")
234 234 sys.exit(1)
235 235 ui.write("it worked :)\n")
236 236 new_rev = b.next()
237 237 ui.write("choosing if good or bad\n")
238 238 if rev in b.ancestors(head=new_rev):
239 239 b.bad(new_rev)
240 240 ui.write("it is bad\n")
241 241 else:
242 242 b.good(new_rev)
243 243 ui.write("it is good\n")
244 244 anc = b.ancestors()
245 245 #repo.update(new_rev, force=True)
246 246 for v in anc:
247 247 if v != rev:
248 248 ui.warn("fail to found cset! :(\n")
249 249 return 1
250 250 ui.write("Found bad cset: %s\n" % hg.hex(b.badrev))
251 251 ui.write("Everything is ok :)\n")
252 252 return 0
253 253
254 254 def bisect_run(ui, repo, cmd=None, *args):
255 """bisect extension: dichotomic search in the DAG of changesets
256 for subcommands see "hg bisect help\"
255 """Dichotomic search in the DAG of changesets
256
257 This extension helps to find changesets which cause problems.
258 To use, mark the earliest changeset you know introduces the problem
259 as bad, then mark the latest changeset which is free from the problem
260 as good. Bisect will update your working directory to a revision for
261 testing. Once you have performed tests, mark the working directory
262 as bad or good and bisect will either update to another candidate
263 changeset or announce that it has found the bad revision.
264
265 Note: bisect expects bad revisions to be descendants of good revisions.
266 If you are looking for the point at which a problem was fixed, then make
267 the problem-free state "bad" and the problematic state "good."
268
269 For subcommands see "hg bisect help\"
257 270 """
258 271 def help_(cmd=None, *args):
259 272 """show help for a given bisect subcommand or all subcommands"""
260 273 cmdtable = bisectcmdtable
261 274 if cmd:
262 275 doc = cmdtable[cmd][0].__doc__
263 276 synopsis = cmdtable[cmd][2]
264 277 ui.write(synopsis + "\n")
265 278 ui.write("\n" + doc + "\n")
266 279 return
267 280 ui.write(_("list of subcommands for the bisect extension\n\n"))
268 281 cmds = cmdtable.keys()
269 282 cmds.sort()
270 283 m = max([len(c) for c in cmds])
271 284 for cmd in cmds:
272 285 doc = cmdtable[cmd][0].__doc__.splitlines(0)[0].rstrip()
273 286 ui.write(" %-*s %s\n" % (m, cmd, doc))
274 287
275 288 b = bisect(ui, repo)
276 289 bisectcmdtable = {
277 290 "init": (b.init, 0, _("hg bisect init")),
278 291 "bad": (b.autobad, 1, _("hg bisect bad [<rev>]")),
279 292 "good": (b.autogood, 1, _("hg bisect good [<rev>]")),
280 293 "next": (b.autonext, 0, _("hg bisect next")),
281 294 "reset": (b.reset, 0, _("hg bisect reset")),
282 295 "help": (help_, 1, _("hg bisect help [<subcommand>]")),
283 296 }
284 297
285 298 if not bisectcmdtable.has_key(cmd):
286 299 ui.warn(_("bisect: Unknown sub-command\n"))
287 300 return help_()
288 301 if len(args) > bisectcmdtable[cmd][1]:
289 302 ui.warn(_("bisect: Too many arguments\n"))
290 303 return help_()
291 304 try:
292 305 return bisectcmdtable[cmd][0](*args)
293 306 finally:
294 307 b.write()
295 308
296 309 cmdtable = {
297 310 "bisect": (bisect_run, [], _("hg bisect [help|init|reset|next|good|bad]")),
298 311 #"bisect-test": (test, [], "hg bisect-test rev"),
299 312 }
General Comments 0
You need to be logged in to leave comments. Login now