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
30
from .trace import note
32
BISECT_INFO_PATH = "bisect"
33
BISECT_REV_PATH = "bisect_revid"
36
class BisectCurrent(object):
37
"""Bisect class for managing the current revision."""
39
def __init__(self, controldir, filename=BISECT_REV_PATH):
40
self._filename = filename
41
self._controldir = controldir
42
self._branch = self._controldir.open_branch()
43
if self._controldir.control_transport.has(filename):
44
self._revid = self._controldir.control_transport.get_bytes(
47
self._revid = self._branch.last_revision()
50
"""Save the current revision."""
51
self._controldir.control_transport.put_bytes(
52
self._filename, self._revid + "\n")
54
def get_current_revid(self):
55
"""Return the current revision id."""
58
def get_current_revno(self):
59
"""Return the current revision number as a tuple."""
60
return self._branch.revision_id_to_dotted_revno(self._revid)
62
def get_parent_revids(self):
63
"""Return the IDs of the current revision's predecessors."""
64
repo = self._branch.repository
65
with repo.lock_read():
66
retval = repo.get_parent_map([self._revid]).get(self._revid, None)
69
def is_merge_point(self):
70
"""Is the current revision a merge point?"""
71
return len(self.get_parent_revids()) > 1
73
def show_rev_log(self, outf):
74
"""Write the current revision's log entry to a file."""
75
rev = self._branch.repository.get_revision(self._revid)
76
revno = ".".join([str(x) for x in self.get_current_revno()])
77
outf.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
80
def switch(self, revid):
81
"""Switch the current revision to the given revid."""
82
working = self._controldir.open_workingtree()
83
if isinstance(revid, int):
84
revid = self._branch.get_rev_id(revid)
85
elif isinstance(revid, list):
86
revid = revid[0].in_history(working.branch).rev_id
87
working.revert(None, working.branch.repository.revision_tree(revid),
93
"""Revert bisection, setting the working tree to normal."""
94
working = self._controldir.open_workingtree()
95
last_rev = working.branch.last_revision()
96
rev_tree = working.branch.repository.revision_tree(last_rev)
97
working.revert(None, rev_tree, False)
98
if self._controldir.control_transport.has(BISECT_REV_PATH):
99
self._controldir.control_transport.delete(BISECT_REV_PATH)
102
class BisectLog(object):
103
"""Bisect log file handler."""
105
def __init__(self, controldir, filename=BISECT_INFO_PATH):
107
self._current = BisectCurrent(controldir)
108
self._controldir = controldir
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 self._controldir.control_transport.get(self._filename)
123
def _load_tree(self):
124
"""Load bzr information."""
126
self._branch = self._controldir.open_branch()
128
def _find_range_and_middle(self, branch_last_rev = None):
129
"""Find the current revision range, and the midpoint."""
131
self._middle_revid = None
133
if not branch_last_rev:
134
last_revid = self._branch.last_revision()
136
last_revid = branch_last_rev
138
repo = self._branch.repository
139
with repo.lock_read():
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)
167
# The spread must include the high revision, to bias
168
# odd numbers of intervening revisions towards the high
171
spread = len(between_revs) + 1
175
middle_index = (spread / 2) - 1
177
if len(between_revs) > 0:
178
self._middle_revid = between_revs[middle_index]
180
self._middle_revid = high_revid
182
self._high_revid = high_revid
183
self._low_revid = low_revid
185
def _switch_wc_to_revno(self, revno, outf):
186
"""Move the working tree to the given revno."""
187
self._current.switch(revno)
188
self._current.show_rev_log(outf=outf)
190
def _set_status(self, revid, status):
191
"""Set the bisect status for the given revid."""
192
if not self.is_done():
193
if status != "done" and revid in [x[0] for x in self._items
194
if x[1] in ['yes', 'no']]:
195
raise RuntimeError("attempting to add revid %s twice" % revid)
196
self._items.append((revid, status))
198
def change_file_name(self, filename):
199
"""Switch log files."""
200
self._filename = filename
203
"""Load the bisection log."""
205
if self._controldir.control_transport.has(self._filename):
206
revlog = self._open_for_read()
208
(revid, status) = line.split()
209
self._items.append((revid, status))
212
"""Save the bisection log."""
214
("%s %s\n" % (revid, status))
215
for (revid, status) in self._items)
217
self._controldir.control_transport.put_bytes(
218
self._filename, contents)
220
sys.stdout.write(contents)
223
"""Report whether we've found the right revision."""
224
return len(self._items) > 0 and self._items[-1][1] == "done"
226
def set_status_from_revspec(self, revspec, status):
227
"""Set the bisection status for the revision in revspec."""
229
revid = revspec[0].in_history(self._branch).rev_id
230
self._set_status(revid, status)
232
def set_current(self, status):
233
"""Set the current revision to the given bisection status."""
234
self._set_status(self._current.get_current_revid(), status)
236
def is_merge_point(self, revid):
237
return len(self.get_parent_revids(revid)) > 1
239
def get_parent_revids(self, revid):
240
repo = self._branch.repository
241
with repo.lock_read():
242
retval = repo.get_parent_map([revid]).get(revid, None)
245
def bisect(self, outf):
246
"""Using the current revision's status, do a bisection."""
247
self._find_range_and_middle()
248
# If we've found the "final" revision, check for a
250
while ((self._middle_revid == self._high_revid
251
or self._middle_revid == self._low_revid)
252
and self.is_merge_point(self._middle_revid)):
253
for parent in self.get_parent_revids(self._middle_revid):
254
if parent == self._low_revid:
257
self._find_range_and_middle(parent)
259
self._switch_wc_to_revno(self._middle_revid, outf)
260
if self._middle_revid == self._high_revid or \
261
self._middle_revid == self._low_revid:
262
self.set_current("done")
265
class cmd_bisect(Command):
266
"""Find an interesting commit using a binary search.
268
Bisecting, in a nutshell, is a way to find the commit at which
269
some testable change was made, such as the introduction of a bug
270
or feature. By identifying a version which did not have the
271
interesting change and a later version which did, a developer
272
can test for the presence of the change at various points in
273
the history, eventually ending up at the precise commit when
274
the change was first introduced.
276
This command uses subcommands to implement the search, each
277
of which changes the state of the bisection. The
281
Start a bisect, possibly clearing out a previous bisect.
283
brz bisect yes [-r rev]
284
The specified revision (or the current revision, if not given)
285
has the characteristic we're looking for,
287
brz bisect no [-r rev]
288
The specified revision (or the current revision, if not given)
289
does not have the characteristic we're looking for,
291
brz bisect move -r rev
292
Switch to a different revision manually. Use if the bisect
293
algorithm chooses a revision that is not suitable. Try to
294
move as little as possible.
297
Clear out a bisection in progress.
299
brz bisect log [-o file]
300
Output a log of the current bisection to standard output, or
301
to the specified file.
303
brz bisect replay <logfile>
304
Replay a previously-saved bisect log, forgetting any bisection
305
that might be in progress.
307
brz bisect run <script>
308
Bisect automatically using <script> to determine 'yes' or 'no'.
309
<script> should exit with:
311
125 for unknown (like build failed so we could not test)
315
takes_args = ['subcommand', 'args*']
316
takes_options = [Option('output', short_name='o',
317
help='Write log to this file.', type=text_type),
318
'revision', 'directory']
320
def _check(self, controldir):
321
"""Check preconditions for most operations to work."""
322
if not controldir.control_transport.has(BISECT_INFO_PATH):
323
raise BzrCommandError("No bisection in progress.")
325
def _set_state(self, controldir, revspec, state):
326
"""Set the state of the given revspec and bisecting.
328
Returns boolean indicating if bisection is done."""
329
bisect_log = BisectLog(controldir)
330
if bisect_log.is_done():
331
note("No further bisection is possible.\n")
332
bisect_log._current.show_rev_log(outf=self.outf)
336
bisect_log.set_status_from_revspec(revspec, state)
338
bisect_log.set_current(state)
339
bisect_log.bisect(self.outf)
343
def run(self, subcommand, args_list, directory='.', revision=None, output=None):
344
"""Handle the bisect command."""
347
if subcommand in ('yes', 'no', 'move') and revision:
349
elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
350
log_fn = args_list[0]
351
elif subcommand in ('move', ) and not revision:
352
raise BzrCommandError(
353
"The 'bisect move' command requires a revision.")
354
elif subcommand in ('run', ):
355
run_script = args_list[0]
356
elif args_list or revision:
357
raise BzrCommandError(
358
"Improper arguments to bisect " + subcommand)
360
controldir, _ = ControlDir.open_containing(directory)
363
if subcommand == "start":
364
self.start(controldir)
365
elif subcommand == "yes":
366
self.yes(controldir, revision)
367
elif subcommand == "no":
368
self.no(controldir, revision)
369
elif subcommand == "move":
370
self.move(controldir, revision)
371
elif subcommand == "reset":
372
self.reset(controldir)
373
elif subcommand == "log":
374
self.log(controldir, output)
375
elif subcommand == "replay":
376
self.replay(controldir, log_fn)
377
elif subcommand == "run":
378
self.run_bisect(controldir, run_script)
380
raise BzrCommandError(
381
"Unknown bisect command: " + subcommand)
383
def reset(self, controldir):
384
"""Reset the bisect state to no state."""
385
self._check(controldir)
386
BisectCurrent(controldir).reset()
387
controldir.control_transport.delete(BISECT_INFO_PATH)
389
def start(self, controldir):
390
"""Reset the bisect state, then prepare for a new bisection."""
391
if controldir.control_transport.has(BISECT_INFO_PATH):
392
BisectCurrent(controldir).reset()
393
controldir.control_transport.delete(BISECT_INFO_PATH)
395
bisect_log = BisectLog(controldir)
396
bisect_log.set_current("start")
399
def yes(self, controldir, revspec):
400
"""Mark that a given revision has the state we're looking for."""
401
self._set_state(controldir, revspec, "yes")
403
def no(self, controldir, revspec):
404
"""Mark that a given revision does not have the state we're looking for."""
405
self._set_state(controldir, revspec, "no")
407
def move(self, controldir, revspec):
408
"""Move to a different revision manually."""
409
current = BisectCurrent(controldir)
410
current.switch(revspec)
411
current.show_rev_log(outf=self.outf)
413
def log(self, controldir, filename):
414
"""Write the current bisect log to a file."""
415
self._check(controldir)
416
bisect_log = BisectLog(controldir)
417
bisect_log.change_file_name(filename)
420
def replay(self, controldir, filename):
421
"""Apply the given log file to a clean state, so the state is
422
exactly as it was when the log was saved."""
423
if controldir.control_transport.has(BISECT_INFO_PATH):
424
BisectCurrent(controldir).reset()
425
controldir.control_transport.delete(BISECT_INFO_PATH)
426
bisect_log = BisectLog(controldir, filename)
427
bisect_log.change_file_name(BISECT_INFO_PATH)
430
bisect_log.bisect(self.outf)
432
def run_bisect(self, controldir, script):
434
note("Starting bisect.")
435
self.start(controldir)
438
process = subprocess.Popen(script, shell=True)
440
retcode = process.returncode
442
done = self._set_state(controldir, None, 'yes')
446
done = self._set_state(controldir, None, 'no')