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