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
revdict = self._branch.get_revision_id_to_revno_map()
61
return revdict[self.get_current_revid()]
63
def get_parent_revids(self):
64
"""Return the IDs of the current revision's predecessors."""
65
repo = self._branch.repository
66
with repo.lock_read():
67
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._branch.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._controldir.open_workingtree()
84
if isinstance(revid, int):
85
revid = self._branch.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._controldir.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 self._controldir.control_transport.has(BISECT_REV_PATH):
100
self._controldir.control_transport.delete(BISECT_REV_PATH)
103
class BisectLog(object):
104
"""Bisect log file handler."""
106
def __init__(self, controldir, filename=BISECT_INFO_PATH):
108
self._current = BisectCurrent(controldir)
109
self._controldir = controldir
111
self._high_revid = None
112
self._low_revid = None
113
self._middle_revid = None
114
self._filename = filename
117
def _open_for_read(self):
118
"""Open log file for reading."""
120
return self._controldir.control_transport.get(self._filename)
124
def _load_tree(self):
125
"""Load bzr information."""
127
self._branch = self._controldir.open_branch()
129
def _find_range_and_middle(self, branch_last_rev = None):
130
"""Find the current revision range, and the midpoint."""
132
self._middle_revid = None
134
if not branch_last_rev:
135
last_revid = self._branch.last_revision()
137
last_revid = branch_last_rev
139
repo = self._branch.repository
140
with repo.lock_read():
141
graph = repo.get_graph()
142
rev_sequence = graph.iter_lefthand_ancestry(last_revid,
143
(_mod_revision.NULL_REVISION,))
147
for revision in rev_sequence:
148
between_revs.insert(0, revision)
149
matches = [x[1] for x in self._items
150
if x[0] == revision and x[1] in ('yes', 'no')]
154
raise RuntimeError("revision %s duplicated" % revision)
155
if matches[0] == "yes":
156
high_revid = revision
158
elif matches[0] == "no":
164
high_revid = last_revid
166
low_revid = self._branch.get_rev_id(1)
168
# The spread must include the high revision, to bias
169
# odd numbers of intervening revisions towards the high
172
spread = len(between_revs) + 1
176
middle_index = (spread / 2) - 1
178
if len(between_revs) > 0:
179
self._middle_revid = between_revs[middle_index]
181
self._middle_revid = high_revid
183
self._high_revid = high_revid
184
self._low_revid = low_revid
186
def _switch_wc_to_revno(self, revno, outf):
187
"""Move the working tree to the given revno."""
188
self._current.switch(revno)
189
self._current.show_rev_log(out=outf)
191
def _set_status(self, revid, status):
192
"""Set the bisect status for the given revid."""
193
if not self.is_done():
194
if status != "done" and revid in [x[0] for x in self._items
195
if x[1] in ['yes', 'no']]:
196
raise RuntimeError("attempting to add revid %s twice" % revid)
197
self._items.append((revid, status))
199
def change_file_name(self, filename):
200
"""Switch log files."""
201
self._filename = filename
204
"""Load the bisection log."""
206
if self._controldir.control_transport.has(self._filename):
207
revlog = self._open_for_read()
209
(revid, status) = line.split()
210
self._items.append((revid, status))
213
"""Save the bisection log."""
215
("%s %s\n" % (revid, status))
216
for (revid, status) in self._items)
218
self._controldir.control_transport.put_bytes(
219
self._filename, contents)
221
sys.stdout.write(contents)
224
"""Report whether we've found the right revision."""
225
return len(self._items) > 0 and self._items[-1][1] == "done"
227
def set_status_from_revspec(self, revspec, status):
228
"""Set the bisection status for the revision in revspec."""
230
revid = revspec[0].in_history(self._branch).rev_id
231
self._set_status(revid, status)
233
def set_current(self, status):
234
"""Set the current revision to the given bisection status."""
235
self._set_status(self._current.get_current_revid(), status)
237
def is_merge_point(self, revid):
238
return len(self.get_parent_revids(revid)) > 1
240
def get_parent_revids(self, revid):
241
repo = self._branch.repository
242
with repo.lock_read():
243
retval = repo.get_parent_map([revid]).get(revid, None)
246
def bisect(self, outf):
247
"""Using the current revision's status, do a bisection."""
248
self._find_range_and_middle()
249
# If we've found the "final" revision, check for a
251
while ((self._middle_revid == self._high_revid
252
or self._middle_revid == self._low_revid)
253
and self.is_merge_point(self._middle_revid)):
254
for parent in self.get_parent_revids(self._middle_revid):
255
if parent == self._low_revid:
258
self._find_range_and_middle(parent)
260
self._switch_wc_to_revno(self._middle_revid, outf)
261
if self._middle_revid == self._high_revid or \
262
self._middle_revid == self._low_revid:
263
self.set_current("done")
266
class cmd_bisect(Command):
267
"""Find an interesting commit using a binary search.
269
Bisecting, in a nutshell, is a way to find the commit at which
270
some testable change was made, such as the introduction of a bug
271
or feature. By identifying a version which did not have the
272
interesting change and a later version which did, a developer
273
can test for the presence of the change at various points in
274
the history, eventually ending up at the precise commit when
275
the change was first introduced.
277
This command uses subcommands to implement the search, each
278
of which changes the state of the bisection. The
282
Start a bisect, possibly clearing out a previous bisect.
284
brz bisect yes [-r rev]
285
The specified revision (or the current revision, if not given)
286
has the characteristic we're looking for,
288
brz bisect no [-r rev]
289
The specified revision (or the current revision, if not given)
290
does not have the characteristic we're looking for,
292
brz bisect move -r rev
293
Switch to a different revision manually. Use if the bisect
294
algorithm chooses a revision that is not suitable. Try to
295
move as little as possible.
298
Clear out a bisection in progress.
300
brz bisect log [-o file]
301
Output a log of the current bisection to standard output, or
302
to the specified file.
304
brz bisect replay <logfile>
305
Replay a previously-saved bisect log, forgetting any bisection
306
that might be in progress.
308
brz bisect run <script>
309
Bisect automatically using <script> to determine 'yes' or 'no'.
310
<script> should exit with:
312
125 for unknown (like build failed so we could not test)
316
takes_args = ['subcommand', 'args*']
317
takes_options = [Option('output', short_name='o',
318
help='Write log to this file.', type=text_type),
319
'revision', 'directory']
321
def _check(self, controldir):
322
"""Check preconditions for most operations to work."""
323
if not controldir.control_transport.has(BISECT_INFO_PATH):
324
raise BzrCommandError("No bisection in progress.")
326
def _set_state(self, controldir, revspec, state):
327
"""Set the state of the given revspec and bisecting.
329
Returns boolean indicating if bisection is done."""
330
bisect_log = BisectLog(controldir)
331
if bisect_log.is_done():
332
note("No further bisection is possible.\n")
333
bisect_log._current.show_rev_log(self.outf)
337
bisect_log.set_status_from_revspec(revspec, state)
339
bisect_log.set_current(state)
340
bisect_log.bisect(self.outf)
344
def run(self, subcommand, args_list, directory='.', revision=None, output=None):
345
"""Handle the bisect command."""
348
if subcommand in ('yes', 'no', 'move') and revision:
350
elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
351
log_fn = args_list[0]
352
elif subcommand in ('move', ) and not revision:
353
raise BzrCommandError(
354
"The 'bisect move' command requires a revision.")
355
elif subcommand in ('run', ):
356
run_script = args_list[0]
357
elif args_list or revision:
358
raise BzrCommandError(
359
"Improper arguments to bisect " + subcommand)
361
controldir, _ = ControlDir.open_containing(directory)
364
if subcommand == "start":
365
self.start(controldir)
366
elif subcommand == "yes":
367
self.yes(controldir, revision)
368
elif subcommand == "no":
369
self.no(controldir, revision)
370
elif subcommand == "move":
371
self.move(controldir, revision)
372
elif subcommand == "reset":
373
self.reset(controldir)
374
elif subcommand == "log":
375
self.log(controldir, output)
376
elif subcommand == "replay":
377
self.replay(controldir, log_fn)
378
elif subcommand == "run":
379
self.run_bisect(controldir, run_script)
381
raise BzrCommandError(
382
"Unknown bisect command: " + subcommand)
384
def reset(self, controldir):
385
"""Reset the bisect state to no state."""
386
self._check(controldir)
387
BisectCurrent(controldir).reset()
388
controldir.control_transport.delete(BISECT_INFO_PATH)
390
def start(self, controldir):
391
"""Reset the bisect state, then prepare for a new bisection."""
392
if controldir.control_transport.has(BISECT_INFO_PATH):
393
BisectCurrent(controldir).reset()
394
controldir.control_transport.delete(BISECT_INFO_PATH)
396
bisect_log = BisectLog(controldir)
397
bisect_log.set_current("start")
400
def yes(self, controldir, revspec):
401
"""Mark that a given revision has the state we're looking for."""
402
self._set_state(controldir, revspec, "yes")
404
def no(self, controldir, revspec):
405
"""Mark that a given revision does not have the state we're looking for."""
406
self._set_state(controldir, revspec, "no")
408
def move(self, controldir, revspec):
409
"""Move to a different revision manually."""
410
current = BisectCurrent(controldir)
411
current.switch(revspec)
412
current.show_rev_log(out=self.outf)
414
def log(self, controldir, filename):
415
"""Write the current bisect log to a file."""
416
self._check(controldir)
417
bisect_log = BisectLog(controldir)
418
bisect_log.change_file_name(filename)
421
def replay(self, controldir, filename):
422
"""Apply the given log file to a clean state, so the state is
423
exactly as it was when the log was saved."""
424
if controldir.control_transport.has(BISECT_INFO_PATH):
425
BisectCurrent(controldir).reset()
426
controldir.control_transport.delete(BISECT_INFO_PATH)
427
bisect_log = BisectLog(controldir, filename)
428
bisect_log.change_file_name(BISECT_INFO_PATH)
431
bisect_log.bisect(self.outf)
433
def run_bisect(self, controldir, script):
435
note("Starting bisect.")
436
self.start(controldir)
439
process = subprocess.Popen(script, shell=True)
441
retcode = process.returncode
443
done = self._set_state(controldir, None, 'yes')
447
done = self._set_state(controldir, None, 'no')