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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""bisect command implementations."""
19
from __future__ import absolute_import
23
from .controldir import ControlDir
24
from . import revision as _mod_revision
25
from .commands import Command
26
from .errors import BzrCommandError
27
from .option import Option
28
from .trace import note
30
bisect_info_path = ".bzr/bisect"
31
bisect_rev_path = ".bzr/bisect_revid"
34
class BisectCurrent(object):
35
"""Bisect class for managing the current revision."""
37
def __init__(self, filename=bisect_rev_path):
38
self._filename = filename
39
self._bzrdir = ControlDir.open_containing(".")[0]
40
self._bzrbranch = self._bzrdir.open_branch()
41
if os.path.exists(filename):
42
revid_file = open(filename)
43
self._revid = revid_file.read().strip()
46
self._revid = self._bzrbranch.last_revision()
49
"""Save the current revision."""
51
revid_file = open(self._filename, "w")
52
revid_file.write(self._revid + "\n")
55
def get_current_revid(self):
56
"""Return the current revision id."""
59
def get_current_revno(self):
60
"""Return the current revision number as a tuple."""
61
revdict = self._bzrbranch.get_revision_id_to_revno_map()
62
return revdict[self.get_current_revid()]
64
def get_parent_revids(self):
65
"""Return the IDs of the current revision's predecessors."""
66
repo = self._bzrbranch.repository
68
retval = repo.get_parent_map([self._revid]).get(self._revid, None)
72
def is_merge_point(self):
73
"""Is the current revision a merge point?"""
74
return len(self.get_parent_revids()) > 1
76
def show_rev_log(self, out = sys.stdout):
77
"""Write the current revision's log entry to a file."""
78
rev = self._bzrbranch.repository.get_revision(self._revid)
79
revno = ".".join([str(x) for x in self.get_current_revno()])
80
out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
83
def switch(self, revid):
84
"""Switch the current revision to the given revid."""
85
working = self._bzrdir.open_workingtree()
86
if isinstance(revid, int):
87
revid = self._bzrbranch.get_rev_id(revid)
88
elif isinstance(revid, list):
89
revid = revid[0].in_history(working.branch).rev_id
90
working.revert(None, working.branch.repository.revision_tree(revid),
96
"""Revert bisection, setting the working tree to normal."""
97
working = self._bzrdir.open_workingtree()
98
last_rev = working.branch.last_revision()
99
rev_tree = working.branch.repository.revision_tree(last_rev)
100
working.revert(None, rev_tree, False)
101
if os.path.exists(bisect_rev_path):
102
os.unlink(bisect_rev_path)
105
class BisectLog(object):
106
"""Bisect log file handler."""
108
def __init__(self, filename = bisect_info_path):
110
self._current = BisectCurrent()
112
self._high_revid = None
113
self._low_revid = None
114
self._middle_revid = None
115
self._filename = filename
118
def _open_for_read(self):
119
"""Open log file for reading."""
121
return open(self._filename)
125
def _open_for_write(self):
126
"""Open log file for writing."""
128
return open(self._filename, "w")
132
def _load_bzr_tree(self):
133
"""Load bzr information."""
135
self._bzrdir = ControlDir.open_containing('.')[0]
136
self._bzrbranch = self._bzrdir.open_branch()
138
def _find_range_and_middle(self, branch_last_rev = None):
139
"""Find the current revision range, and the midpoint."""
140
self._load_bzr_tree()
141
self._middle_revid = None
143
if not branch_last_rev:
144
last_revid = self._bzrbranch.last_revision()
146
last_revid = branch_last_rev
148
repo = self._bzrbranch.repository
151
graph = repo.get_graph()
152
rev_sequence = graph.iter_lefthand_ancestry(last_revid,
153
(_mod_revision.NULL_REVISION,))
157
for revision in rev_sequence:
158
between_revs.insert(0, revision)
159
matches = [x[1] for x in self._items
160
if x[0] == revision and x[1] in ('yes', 'no')]
164
raise RuntimeError("revision %s duplicated" % revision)
165
if matches[0] == "yes":
166
high_revid = revision
168
elif matches[0] == "no":
174
high_revid = last_revid
176
low_revid = self._bzrbranch.get_rev_id(1)
180
# The spread must include the high revision, to bias
181
# odd numbers of intervening revisions towards the high
184
spread = len(between_revs) + 1
188
middle_index = (spread / 2) - 1
190
if len(between_revs) > 0:
191
self._middle_revid = between_revs[middle_index]
193
self._middle_revid = high_revid
195
self._high_revid = high_revid
196
self._low_revid = low_revid
198
def _switch_wc_to_revno(self, revno, outf):
199
"""Move the working tree to the given revno."""
200
self._current.switch(revno)
201
self._current.show_rev_log(out=outf)
203
def _set_status(self, revid, status):
204
"""Set the bisect status for the given revid."""
205
if not self.is_done():
206
if status != "done" and revid in [x[0] for x in self._items
207
if x[1] in ['yes', 'no']]:
208
raise RuntimeError("attempting to add revid %s twice" % revid)
209
self._items.append((revid, status))
211
def change_file_name(self, filename):
212
"""Switch log files."""
213
self._filename = filename
216
"""Load the bisection log."""
218
if os.path.exists(self._filename):
219
revlog = self._open_for_read()
221
(revid, status) = line.split()
222
self._items.append((revid, status))
225
"""Save the bisection log."""
226
revlog = self._open_for_write()
227
for (revid, status) in self._items:
228
revlog.write("%s %s\n" % (revid, status))
231
"""Report whether we've found the right revision."""
232
return len(self._items) > 0 and self._items[-1][1] == "done"
234
def set_status_from_revspec(self, revspec, status):
235
"""Set the bisection status for the revision in revspec."""
236
self._load_bzr_tree()
237
revid = revspec[0].in_history(self._bzrbranch).rev_id
238
self._set_status(revid, status)
240
def set_current(self, status):
241
"""Set the current revision to the given bisection status."""
242
self._set_status(self._current.get_current_revid(), status)
244
def is_merge_point(self, revid):
245
return len(self.get_parent_revids(revid)) > 1
247
def get_parent_revids(self, revid):
248
repo = self._bzrbranch.repository
251
retval = repo.get_parent_map([revid]).get(revid, None)
256
def bisect(self, outf):
257
"""Using the current revision's status, do a bisection."""
258
self._find_range_and_middle()
259
# If we've found the "final" revision, check for a
261
while ((self._middle_revid == self._high_revid
262
or self._middle_revid == self._low_revid)
263
and self.is_merge_point(self._middle_revid)):
264
for parent in self.get_parent_revids(self._middle_revid):
265
if parent == self._low_revid:
268
self._find_range_and_middle(parent)
270
self._switch_wc_to_revno(self._middle_revid, outf)
271
if self._middle_revid == self._high_revid or \
272
self._middle_revid == self._low_revid:
273
self.set_current("done")
276
class cmd_bisect(Command):
277
"""Find an interesting commit using a binary search.
279
Bisecting, in a nutshell, is a way to find the commit at which
280
some testable change was made, such as the introduction of a bug
281
or feature. By identifying a version which did not have the
282
interesting change and a later version which did, a developer
283
can test for the presence of the change at various points in
284
the history, eventually ending up at the precise commit when
285
the change was first introduced.
287
This command uses subcommands to implement the search, each
288
of which changes the state of the bisection. The
292
Start a bisect, possibly clearing out a previous bisect.
294
brz bisect yes [-r rev]
295
The specified revision (or the current revision, if not given)
296
has the characteristic we're looking for,
298
brz bisect no [-r rev]
299
The specified revision (or the current revision, if not given)
300
does not have the characteristic we're looking for,
302
brz bisect move -r rev
303
Switch to a different revision manually. Use if the bisect
304
algorithm chooses a revision that is not suitable. Try to
305
move as little as possible.
308
Clear out a bisection in progress.
310
brz bisect log [-o file]
311
Output a log of the current bisection to standard output, or
312
to the specified file.
314
brz bisect replay <logfile>
315
Replay a previously-saved bisect log, forgetting any bisection
316
that might be in progress.
318
brz bisect run <script>
319
Bisect automatically using <script> to determine 'yes' or 'no'.
320
<script> should exit with:
322
125 for unknown (like build failed so we could not test)
326
takes_args = ['subcommand', 'args*']
327
takes_options = [Option('output', short_name='o',
328
help='Write log to this file.', type=unicode),
332
"""Check preconditions for most operations to work."""
333
if not os.path.exists(bisect_info_path):
334
raise BzrCommandError("No bisection in progress.")
336
def _set_state(self, revspec, state):
337
"""Set the state of the given revspec and bisecting.
339
Returns boolean indicating if bisection is done."""
340
bisect_log = BisectLog()
341
if bisect_log.is_done():
342
note("No further bisection is possible.\n")
343
bisect_log._current.show_rev_log(self.outf)
347
bisect_log.set_status_from_revspec(revspec, state)
349
bisect_log.set_current(state)
350
bisect_log.bisect(self.outf)
354
def run(self, subcommand, args_list, revision=None, output=None):
355
"""Handle the bisect command."""
358
if subcommand in ('yes', 'no', 'move') and revision:
360
elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
361
log_fn = args_list[0]
362
elif subcommand in ('move', ) and not revision:
363
raise BzrCommandError(
364
"The 'bisect move' command requires a revision.")
365
elif subcommand in ('run', ):
366
run_script = args_list[0]
367
elif args_list or revision:
368
raise BzrCommandError(
369
"Improper arguments to bisect " + subcommand)
373
if subcommand == "start":
375
elif subcommand == "yes":
377
elif subcommand == "no":
379
elif subcommand == "move":
381
elif subcommand == "reset":
383
elif subcommand == "log":
385
elif subcommand == "replay":
387
elif subcommand == "run":
388
self.run_bisect(run_script)
390
raise BzrCommandError(
391
"Unknown bisect command: " + subcommand)
394
"""Reset the bisect state to no state."""
396
BisectCurrent().reset()
397
os.unlink(bisect_info_path)
400
"""Reset the bisect state, then prepare for a new bisection."""
401
if os.path.exists(bisect_info_path):
402
BisectCurrent().reset()
403
os.unlink(bisect_info_path)
405
bisect_log = BisectLog()
406
bisect_log.set_current("start")
409
def yes(self, revspec):
410
"""Mark that a given revision has the state we're looking for."""
411
self._set_state(revspec, "yes")
413
def no(self, revspec):
414
"""Mark that a given revision does not have the state we're looking for."""
415
self._set_state(revspec, "no")
417
def move(self, revspec):
418
"""Move to a different revision manually."""
419
current = BisectCurrent()
420
current.switch(revspec)
421
current.show_rev_log(out=self.outf)
423
def log(self, filename):
424
"""Write the current bisect log to a file."""
426
bisect_log = BisectLog()
427
bisect_log.change_file_name(filename)
430
def replay(self, filename):
431
"""Apply the given log file to a clean state, so the state is
432
exactly as it was when the log was saved."""
433
if os.path.exists(bisect_info_path):
434
BisectCurrent().reset()
435
os.unlink(bisect_info_path)
436
bisect_log = BisectLog(filename)
437
bisect_log.change_file_name(bisect_info_path)
440
bisect_log.bisect(self.outf)
442
def run_bisect(self, script):
444
note("Starting bisect.")
448
process = subprocess.Popen(script, shell=True)
450
retcode = process.returncode
452
done = self._set_state(None, 'yes')
456
done = self._set_state(None, 'no')