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
22
from .controldir import ControlDir
23
from . import revision as _mod_revision
24
from .commands import Command
25
from .errors import BzrCommandError
26
from .option import Option
27
from .trace import note
29
BISECT_INFO_PATH = "bisect"
30
BISECT_REV_PATH = "bisect_revid"
33
class BisectCurrent(object):
34
"""Bisect class for managing the current revision."""
36
def __init__(self, controldir, filename=BISECT_REV_PATH):
37
self._filename = filename
38
self._controldir = controldir
39
self._branch = self._controldir.open_branch()
40
if self._controldir.control_transport.has(filename):
41
self._revid = self._controldir.control_transport.get_bytes(
44
self._revid = self._branch.last_revision()
47
"""Save the current revision."""
48
self._controldir.control_transport.put_bytes(
49
self._filename, self._revid + "\n")
51
def get_current_revid(self):
52
"""Return the current revision id."""
55
def get_current_revno(self):
56
"""Return the current revision number as a tuple."""
57
revdict = self._branch.get_revision_id_to_revno_map()
58
return revdict[self.get_current_revid()]
60
def get_parent_revids(self):
61
"""Return the IDs of the current revision's predecessors."""
62
repo = self._branch.repository
64
retval = repo.get_parent_map([self._revid]).get(self._revid, None)
68
def is_merge_point(self):
69
"""Is the current revision a merge point?"""
70
return len(self.get_parent_revids()) > 1
72
def show_rev_log(self, out = sys.stdout):
73
"""Write the current revision's log entry to a file."""
74
rev = self._branch.repository.get_revision(self._revid)
75
revno = ".".join([str(x) for x in self.get_current_revno()])
76
out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
79
def switch(self, revid):
80
"""Switch the current revision to the given revid."""
81
working = self._controldir.open_workingtree()
82
if isinstance(revid, int):
83
revid = self._branch.get_rev_id(revid)
84
elif isinstance(revid, list):
85
revid = revid[0].in_history(working.branch).rev_id
86
working.revert(None, working.branch.repository.revision_tree(revid),
92
"""Revert bisection, setting the working tree to normal."""
93
working = self._controldir.open_workingtree()
94
last_rev = working.branch.last_revision()
95
rev_tree = working.branch.repository.revision_tree(last_rev)
96
working.revert(None, rev_tree, False)
97
if self._controldir.control_transport.has(BISECT_REV_PATH):
98
self._controldir.control_transport.delete(BISECT_REV_PATH)
101
class BisectLog(object):
102
"""Bisect log file handler."""
104
def __init__(self, controldir, filename=BISECT_INFO_PATH):
106
self._current = BisectCurrent(controldir)
107
self._controldir = controldir
109
self._high_revid = None
110
self._low_revid = None
111
self._middle_revid = None
112
self._filename = filename
115
def _open_for_read(self):
116
"""Open log file for reading."""
118
return self._controldir.control_transport.get(self._filename)
122
def _load_tree(self):
123
"""Load bzr information."""
125
self._branch = self._controldir.open_branch()
127
def _find_range_and_middle(self, branch_last_rev = None):
128
"""Find the current revision range, and the midpoint."""
130
self._middle_revid = None
132
if not branch_last_rev:
133
last_revid = self._branch.last_revision()
135
last_revid = branch_last_rev
137
repo = self._branch.repository
140
graph = repo.get_graph()
141
rev_sequence = graph.iter_lefthand_ancestry(last_revid,
142
(_mod_revision.NULL_REVISION,))
146
for revision in rev_sequence:
147
between_revs.insert(0, revision)
148
matches = [x[1] for x in self._items
149
if x[0] == revision and x[1] in ('yes', 'no')]
153
raise RuntimeError("revision %s duplicated" % revision)
154
if matches[0] == "yes":
155
high_revid = revision
157
elif matches[0] == "no":
163
high_revid = last_revid
165
low_revid = self._branch.get_rev_id(1)
169
# The spread must include the high revision, to bias
170
# odd numbers of intervening revisions towards the high
173
spread = len(between_revs) + 1
177
middle_index = (spread / 2) - 1
179
if len(between_revs) > 0:
180
self._middle_revid = between_revs[middle_index]
182
self._middle_revid = high_revid
184
self._high_revid = high_revid
185
self._low_revid = low_revid
187
def _switch_wc_to_revno(self, revno, outf):
188
"""Move the working tree to the given revno."""
189
self._current.switch(revno)
190
self._current.show_rev_log(out=outf)
192
def _set_status(self, revid, status):
193
"""Set the bisect status for the given revid."""
194
if not self.is_done():
195
if status != "done" and revid in [x[0] for x in self._items
196
if x[1] in ['yes', 'no']]:
197
raise RuntimeError("attempting to add revid %s twice" % revid)
198
self._items.append((revid, status))
200
def change_file_name(self, filename):
201
"""Switch log files."""
202
self._filename = filename
205
"""Load the bisection log."""
207
if self._controldir.control_transport.has(self._filename):
208
revlog = self._open_for_read()
210
(revid, status) = line.split()
211
self._items.append((revid, status))
214
"""Save the bisection log."""
216
("%s %s\n" % (revid, status))
217
for (revid, status) in self._items)
219
self._controldir.control_transport.put_bytes(
220
self._filename, contents)
222
sys.stdout.write(contents)
225
"""Report whether we've found the right revision."""
226
return len(self._items) > 0 and self._items[-1][1] == "done"
228
def set_status_from_revspec(self, revspec, status):
229
"""Set the bisection status for the revision in revspec."""
231
revid = revspec[0].in_history(self._branch).rev_id
232
self._set_status(revid, status)
234
def set_current(self, status):
235
"""Set the current revision to the given bisection status."""
236
self._set_status(self._current.get_current_revid(), status)
238
def is_merge_point(self, revid):
239
return len(self.get_parent_revids(revid)) > 1
241
def get_parent_revids(self, revid):
242
repo = self._branch.repository
245
retval = repo.get_parent_map([revid]).get(revid, None)
250
def bisect(self, outf):
251
"""Using the current revision's status, do a bisection."""
252
self._find_range_and_middle()
253
# If we've found the "final" revision, check for a
255
while ((self._middle_revid == self._high_revid
256
or self._middle_revid == self._low_revid)
257
and self.is_merge_point(self._middle_revid)):
258
for parent in self.get_parent_revids(self._middle_revid):
259
if parent == self._low_revid:
262
self._find_range_and_middle(parent)
264
self._switch_wc_to_revno(self._middle_revid, outf)
265
if self._middle_revid == self._high_revid or \
266
self._middle_revid == self._low_revid:
267
self.set_current("done")
270
class cmd_bisect(Command):
271
"""Find an interesting commit using a binary search.
273
Bisecting, in a nutshell, is a way to find the commit at which
274
some testable change was made, such as the introduction of a bug
275
or feature. By identifying a version which did not have the
276
interesting change and a later version which did, a developer
277
can test for the presence of the change at various points in
278
the history, eventually ending up at the precise commit when
279
the change was first introduced.
281
This command uses subcommands to implement the search, each
282
of which changes the state of the bisection. The
286
Start a bisect, possibly clearing out a previous bisect.
288
brz bisect yes [-r rev]
289
The specified revision (or the current revision, if not given)
290
has the characteristic we're looking for,
292
brz bisect no [-r rev]
293
The specified revision (or the current revision, if not given)
294
does not have the characteristic we're looking for,
296
brz bisect move -r rev
297
Switch to a different revision manually. Use if the bisect
298
algorithm chooses a revision that is not suitable. Try to
299
move as little as possible.
302
Clear out a bisection in progress.
304
brz bisect log [-o file]
305
Output a log of the current bisection to standard output, or
306
to the specified file.
308
brz bisect replay <logfile>
309
Replay a previously-saved bisect log, forgetting any bisection
310
that might be in progress.
312
brz bisect run <script>
313
Bisect automatically using <script> to determine 'yes' or 'no'.
314
<script> should exit with:
316
125 for unknown (like build failed so we could not test)
320
takes_args = ['subcommand', 'args*']
321
takes_options = [Option('output', short_name='o',
322
help='Write log to this file.', type=unicode),
323
'revision', 'directory']
325
def _check(self, controldir):
326
"""Check preconditions for most operations to work."""
327
if not controldir.control_transport.has(BISECT_INFO_PATH):
328
raise BzrCommandError("No bisection in progress.")
330
def _set_state(self, controldir, revspec, state):
331
"""Set the state of the given revspec and bisecting.
333
Returns boolean indicating if bisection is done."""
334
bisect_log = BisectLog(controldir)
335
if bisect_log.is_done():
336
note("No further bisection is possible.\n")
337
bisect_log._current.show_rev_log(self.outf)
341
bisect_log.set_status_from_revspec(revspec, state)
343
bisect_log.set_current(state)
344
bisect_log.bisect(self.outf)
348
def run(self, subcommand, args_list, directory='.', revision=None, output=None):
349
"""Handle the bisect command."""
352
if subcommand in ('yes', 'no', 'move') and revision:
354
elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
355
log_fn = args_list[0]
356
elif subcommand in ('move', ) and not revision:
357
raise BzrCommandError(
358
"The 'bisect move' command requires a revision.")
359
elif subcommand in ('run', ):
360
run_script = args_list[0]
361
elif args_list or revision:
362
raise BzrCommandError(
363
"Improper arguments to bisect " + subcommand)
365
controldir, _ = ControlDir.open_containing(directory)
368
if subcommand == "start":
369
self.start(controldir)
370
elif subcommand == "yes":
371
self.yes(controldir, revision)
372
elif subcommand == "no":
373
self.no(controldir, revision)
374
elif subcommand == "move":
375
self.move(controldir, revision)
376
elif subcommand == "reset":
377
self.reset(controldir)
378
elif subcommand == "log":
379
self.log(controldir, output)
380
elif subcommand == "replay":
381
self.replay(controldir, log_fn)
382
elif subcommand == "run":
383
self.run_bisect(controldir, run_script)
385
raise BzrCommandError(
386
"Unknown bisect command: " + subcommand)
388
def reset(self, controldir):
389
"""Reset the bisect state to no state."""
390
self._check(controldir)
391
BisectCurrent(controldir).reset()
392
controldir.control_transport.delete(BISECT_INFO_PATH)
394
def start(self, controldir):
395
"""Reset the bisect state, then prepare for a new bisection."""
396
if controldir.control_transport.has(BISECT_INFO_PATH):
397
BisectCurrent(controldir).reset()
398
controldir.control_transport.delete(BISECT_INFO_PATH)
400
bisect_log = BisectLog(controldir)
401
bisect_log.set_current("start")
404
def yes(self, controldir, revspec):
405
"""Mark that a given revision has the state we're looking for."""
406
self._set_state(controldir, revspec, "yes")
408
def no(self, controldir, revspec):
409
"""Mark that a given revision does not have the state we're looking for."""
410
self._set_state(controldir, revspec, "no")
412
def move(self, controldir, revspec):
413
"""Move to a different revision manually."""
414
current = BisectCurrent(controldir)
415
current.switch(revspec)
416
current.show_rev_log(out=self.outf)
418
def log(self, controldir, filename):
419
"""Write the current bisect log to a file."""
420
self._check(controldir)
421
bisect_log = BisectLog(controldir)
422
bisect_log.change_file_name(filename)
425
def replay(self, controldir, filename):
426
"""Apply the given log file to a clean state, so the state is
427
exactly as it was when the log was saved."""
428
if controldir.control_transport.has(BISECT_INFO_PATH):
429
BisectCurrent(controldir).reset()
430
controldir.control_transport.delete(BISECT_INFO_PATH)
431
bisect_log = BisectLog(controldir, filename)
432
bisect_log.change_file_name(BISECT_INFO_PATH)
435
bisect_log.bisect(self.outf)
437
def run_bisect(self, controldir, script):
439
note("Starting bisect.")
440
self.start(controldir)
443
process = subprocess.Popen(script, shell=True)
445
retcode = process.returncode
447
done = self._set_state(controldir, None, 'yes')
451
done = self._set_state(controldir, None, 'no')