1
# Copyright (C) 2006-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""bisect command implementations."""
22
from bzrlib import revision as _mod_revision
23
from bzrlib.commands import Command
24
from bzrlib.errors import BzrCommandError
25
from bzrlib.option import Option
26
from bzrlib.trace import note
28
bisect_info_path = ".bzr/bisect"
29
bisect_rev_path = ".bzr/bisect_revid"
32
class BisectCurrent(object):
33
"""Bisect class for managing the current revision."""
35
def __init__(self, filename = bisect_rev_path):
36
self._filename = filename
37
self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing(".")[0]
38
self._bzrbranch = self._bzrdir.open_branch()
39
if os.path.exists(filename):
40
revid_file = open(filename)
41
self._revid = revid_file.read().strip()
44
self._revid = self._bzrbranch.last_revision()
47
"""Save the current revision."""
49
revid_file = open(self._filename, "w")
50
revid_file.write(self._revid + "\n")
53
def get_current_revid(self):
54
"""Return the current revision id."""
57
def get_current_revno(self):
58
"""Return the current revision number as a tuple."""
59
revdict = self._bzrbranch.get_revision_id_to_revno_map()
60
return revdict[self.get_current_revid()]
62
def get_parent_revids(self):
63
"""Return the IDs of the current revision's predecessors."""
64
repo = self._bzrbranch.repository
66
retval = repo.get_parent_map([self._revid]).get(self._revid, None)
70
def is_merge_point(self):
71
"""Is the current revision a merge point?"""
72
return len(self.get_parent_revids()) > 1
74
def show_rev_log(self, out = sys.stdout):
75
"""Write the current revision's log entry to a file."""
76
rev = self._bzrbranch.repository.get_revision(self._revid)
77
revno = ".".join([str(x) for x in self.get_current_revno()])
78
out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
81
def switch(self, revid):
82
"""Switch the current revision to the given revid."""
83
working = self._bzrdir.open_workingtree()
84
if isinstance(revid, int):
85
revid = self._bzrbranch.get_rev_id(revid)
86
elif isinstance(revid, list):
87
revid = revid[0].in_history(working.branch).rev_id
88
working.revert(None, working.branch.repository.revision_tree(revid),
94
"""Revert bisection, setting the working tree to normal."""
95
working = self._bzrdir.open_workingtree()
96
last_rev = working.branch.last_revision()
97
rev_tree = working.branch.repository.revision_tree(last_rev)
98
working.revert(None, rev_tree, False)
99
if os.path.exists(bisect_rev_path):
100
os.unlink(bisect_rev_path)
103
class BisectLog(object):
104
"""Bisect log file handler."""
106
def __init__(self, filename = bisect_info_path):
108
self._current = BisectCurrent()
110
self._high_revid = None
111
self._low_revid = None
112
self._middle_revid = None
113
self._filename = filename
116
def _open_for_read(self):
117
"""Open log file for reading."""
119
return open(self._filename)
123
def _open_for_write(self):
124
"""Open log file for writing."""
126
return open(self._filename, "w")
130
def _load_bzr_tree(self):
131
"""Load bzr information."""
133
self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing('.')[0]
134
self._bzrbranch = self._bzrdir.open_branch()
136
def _find_range_and_middle(self, branch_last_rev = None):
137
"""Find the current revision range, and the midpoint."""
138
self._load_bzr_tree()
139
self._middle_revid = None
141
if not branch_last_rev:
142
last_revid = self._bzrbranch.last_revision()
144
last_revid = branch_last_rev
146
repo = self._bzrbranch.repository
149
graph = repo.get_graph()
150
rev_sequence = graph.iter_lefthand_ancestry(last_revid,
151
(_mod_revision.NULL_REVISION,))
155
for revision in rev_sequence:
156
between_revs.insert(0, revision)
157
matches = [x[1] for x in self._items
158
if x[0] == revision and x[1] in ('yes', 'no')]
162
raise RuntimeError("revision %s duplicated" % revision)
163
if matches[0] == "yes":
164
high_revid = revision
166
elif matches[0] == "no":
172
high_revid = last_revid
174
low_revid = self._bzrbranch.get_rev_id(1)
178
# The spread must include the high revision, to bias
179
# odd numbers of intervening revisions towards the high
182
spread = len(between_revs) + 1
186
middle_index = (spread / 2) - 1
188
if len(between_revs) > 0:
189
self._middle_revid = between_revs[middle_index]
191
self._middle_revid = high_revid
193
self._high_revid = high_revid
194
self._low_revid = low_revid
196
def _switch_wc_to_revno(self, revno, outf):
197
"""Move the working tree to the given revno."""
198
self._current.switch(revno)
199
self._current.show_rev_log(out=outf)
201
def _set_status(self, revid, status):
202
"""Set the bisect status for the given revid."""
203
if not self.is_done():
204
if status != "done" and revid in [x[0] for x in self._items
205
if x[1] in ['yes', 'no']]:
206
raise RuntimeError("attempting to add revid %s twice" % revid)
207
self._items.append((revid, status))
209
def change_file_name(self, filename):
210
"""Switch log files."""
211
self._filename = filename
214
"""Load the bisection log."""
216
if os.path.exists(self._filename):
217
revlog = self._open_for_read()
219
(revid, status) = line.split()
220
self._items.append((revid, status))
223
"""Save the bisection log."""
224
revlog = self._open_for_write()
225
for (revid, status) in self._items:
226
revlog.write("%s %s\n" % (revid, status))
229
"""Report whether we've found the right revision."""
230
return len(self._items) > 0 and self._items[-1][1] == "done"
232
def set_status_from_revspec(self, revspec, status):
233
"""Set the bisection status for the revision in revspec."""
234
self._load_bzr_tree()
235
revid = revspec[0].in_history(self._bzrbranch).rev_id
236
self._set_status(revid, status)
238
def set_current(self, status):
239
"""Set the current revision to the given bisection status."""
240
self._set_status(self._current.get_current_revid(), status)
242
def is_merge_point(self, revid):
243
return len(self.get_parent_revids(revid)) > 1
245
def get_parent_revids(self, revid):
246
repo = self._bzrbranch.repository
249
retval = repo.get_parent_map([revid]).get(revid, None)
254
def bisect(self, outf):
255
"""Using the current revision's status, do a bisection."""
256
self._find_range_and_middle()
257
# If we've found the "final" revision, check for a
259
while ((self._middle_revid == self._high_revid
260
or self._middle_revid == self._low_revid)
261
and self.is_merge_point(self._middle_revid)):
262
for parent in self.get_parent_revids(self._middle_revid):
263
if parent == self._low_revid:
266
self._find_range_and_middle(parent)
268
self._switch_wc_to_revno(self._middle_revid, outf)
269
if self._middle_revid == self._high_revid or \
270
self._middle_revid == self._low_revid:
271
self.set_current("done")
274
class cmd_bisect(Command):
275
"""Find an interesting commit using a binary search.
277
Bisecting, in a nutshell, is a way to find the commit at which
278
some testable change was made, such as the introduction of a bug
279
or feature. By identifying a version which did not have the
280
interesting change and a later version which did, a developer
281
can test for the presence of the change at various points in
282
the history, eventually ending up at the precise commit when
283
the change was first introduced.
285
This command uses subcommands to implement the search, each
286
of which changes the state of the bisection. The
290
Start a bisect, possibly clearing out a previous bisect.
292
bzr bisect yes [-r rev]
293
The specified revision (or the current revision, if not given)
294
has the characteristic we're looking for,
296
bzr bisect no [-r rev]
297
The specified revision (or the current revision, if not given)
298
does not have the characteristic we're looking for,
300
bzr bisect move -r rev
301
Switch to a different revision manually. Use if the bisect
302
algorithm chooses a revision that is not suitable. Try to
303
move as little as possible.
306
Clear out a bisection in progress.
308
bzr bisect log [-o file]
309
Output a log of the current bisection to standard output, or
310
to the specified file.
312
bzr bisect replay <logfile>
313
Replay a previously-saved bisect log, forgetting any bisection
314
that might be in progress.
316
bzr bisect run <script>
317
Bisect automatically using <script> to determine 'yes' or 'no'.
318
<script> should exit with:
320
125 for unknown (like build failed so we could not test)
324
takes_args = ['subcommand', 'args*']
325
takes_options = [Option('output', short_name='o',
326
help='Write log to this file.', type=unicode),
330
"""Check preconditions for most operations to work."""
331
if not os.path.exists(bisect_info_path):
332
raise BzrCommandError("No bisection in progress.")
334
def _set_state(self, revspec, state):
335
"""Set the state of the given revspec and bisecting.
337
Returns boolean indicating if bisection is done."""
338
bisect_log = BisectLog()
339
if bisect_log.is_done():
340
note("No further bisection is possible.\n")
341
bisect_log._current.show_rev_log(self.outf)
345
bisect_log.set_status_from_revspec(revspec, state)
347
bisect_log.set_current(state)
348
bisect_log.bisect(self.outf)
352
def run(self, subcommand, args_list, revision=None, output=None):
353
"""Handle the bisect command."""
356
if subcommand in ('yes', 'no', 'move') and revision:
358
elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
359
log_fn = args_list[0]
360
elif subcommand in ('move', ) and not revision:
361
raise BzrCommandError(
362
"The 'bisect move' command requires a revision.")
363
elif subcommand in ('run', ):
364
run_script = args_list[0]
365
elif args_list or revision:
366
raise BzrCommandError(
367
"Improper arguments to bisect " + subcommand)
371
if subcommand == "start":
373
elif subcommand == "yes":
375
elif subcommand == "no":
377
elif subcommand == "move":
379
elif subcommand == "reset":
381
elif subcommand == "log":
383
elif subcommand == "replay":
385
elif subcommand == "run":
386
self.run_bisect(run_script)
388
raise BzrCommandError(
389
"Unknown bisect command: " + subcommand)
392
"""Reset the bisect state to no state."""
394
BisectCurrent().reset()
395
os.unlink(bisect_info_path)
398
"""Reset the bisect state, then prepare for a new bisection."""
399
if os.path.exists(bisect_info_path):
400
BisectCurrent().reset()
401
os.unlink(bisect_info_path)
403
bisect_log = BisectLog()
404
bisect_log.set_current("start")
407
def yes(self, revspec):
408
"""Mark that a given revision has the state we're looking for."""
409
self._set_state(revspec, "yes")
411
def no(self, revspec):
412
"""Mark that a given revision does not have the state we're looking for."""
413
self._set_state(revspec, "no")
415
def move(self, revspec):
416
"""Move to a different revision manually."""
417
current = BisectCurrent()
418
current.switch(revspec)
419
current.show_rev_log(out=self.outf)
421
def log(self, filename):
422
"""Write the current bisect log to a file."""
424
bisect_log = BisectLog()
425
bisect_log.change_file_name(filename)
428
def replay(self, filename):
429
"""Apply the given log file to a clean state, so the state is
430
exactly as it was when the log was saved."""
431
if os.path.exists(bisect_info_path):
432
BisectCurrent().reset()
433
os.unlink(bisect_info_path)
434
bisect_log = BisectLog(filename)
435
bisect_log.change_file_name(bisect_info_path)
438
bisect_log.bisect(self.outf)
440
def run_bisect(self, script):
442
note("Starting bisect.")
446
process = subprocess.Popen(script, shell=True)
448
retcode = process.returncode
450
done = self._set_state(None, 'yes')
454
done = self._set_state(None, 'no')