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 + b"\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
return self._branch.revision_id_to_dotted_revno(self._revid)
59
def get_parent_revids(self):
60
"""Return the IDs of the current revision's predecessors."""
61
repo = self._branch.repository
62
with repo.lock_read():
63
retval = repo.get_parent_map([self._revid]).get(self._revid, None)
66
def is_merge_point(self):
67
"""Is the current revision a merge point?"""
68
return len(self.get_parent_revids()) > 1
70
def show_rev_log(self, outf):
71
"""Write the current revision's log entry to a file."""
72
rev = self._branch.repository.get_revision(self._revid)
73
revno = ".".join([str(x) for x in self.get_current_revno()])
74
outf.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
77
def switch(self, revid):
78
"""Switch the current revision to the given revid."""
79
working = self._controldir.open_workingtree()
80
if isinstance(revid, int):
81
revid = self._branch.get_rev_id(revid)
82
elif isinstance(revid, list):
83
revid = revid[0].in_history(working.branch).rev_id
84
working.revert(None, working.branch.repository.revision_tree(revid),
90
"""Revert bisection, setting the working tree to normal."""
91
working = self._controldir.open_workingtree()
92
last_rev = working.branch.last_revision()
93
rev_tree = working.branch.repository.revision_tree(last_rev)
94
working.revert(None, rev_tree, False)
95
if self._controldir.control_transport.has(BISECT_REV_PATH):
96
self._controldir.control_transport.delete(BISECT_REV_PATH)
99
class BisectLog(object):
100
"""Bisect log file handler."""
102
def __init__(self, controldir, filename=BISECT_INFO_PATH):
104
self._current = BisectCurrent(controldir)
105
self._controldir = controldir
107
self._high_revid = None
108
self._low_revid = None
109
self._middle_revid = None
110
self._filename = filename
113
def _open_for_read(self):
114
"""Open log file for reading."""
116
return self._controldir.control_transport.get(self._filename)
120
def _load_tree(self):
121
"""Load bzr information."""
123
self._branch = self._controldir.open_branch()
125
def _find_range_and_middle(self, branch_last_rev=None):
126
"""Find the current revision range, and the midpoint."""
128
self._middle_revid = None
130
if not branch_last_rev:
131
last_revid = self._branch.last_revision()
133
last_revid = branch_last_rev
135
repo = self._branch.repository
136
with repo.lock_read():
137
graph = repo.get_graph()
138
rev_sequence = graph.iter_lefthand_ancestry(
139
last_revid, (_mod_revision.NULL_REVISION,))
143
for revision in rev_sequence:
144
between_revs.insert(0, revision)
145
matches = [x[1] for x in self._items
146
if x[0] == revision and x[1] in ('yes', 'no')]
150
raise RuntimeError("revision %s duplicated" % revision)
151
if matches[0] == "yes":
152
high_revid = revision
154
elif matches[0] == "no":
160
high_revid = last_revid
162
low_revid = self._branch.get_rev_id(1)
164
# The spread must include the high revision, to bias
165
# odd numbers of intervening revisions towards the high
168
spread = len(between_revs) + 1
172
middle_index = (spread // 2) - 1
174
if len(between_revs) > 0:
175
self._middle_revid = between_revs[middle_index]
177
self._middle_revid = high_revid
179
self._high_revid = high_revid
180
self._low_revid = low_revid
182
def _switch_wc_to_revno(self, revno, outf):
183
"""Move the working tree to the given revno."""
184
self._current.switch(revno)
185
self._current.show_rev_log(outf=outf)
187
def _set_status(self, revid, status):
188
"""Set the bisect status for the given revid."""
189
if not self.is_done():
190
if status != "done" and revid in [x[0] for x in self._items
191
if x[1] in ['yes', 'no']]:
192
raise RuntimeError("attempting to add revid %s twice" % revid)
193
self._items.append((revid, status))
195
def change_file_name(self, filename):
196
"""Switch log files."""
197
self._filename = filename
200
"""Load the bisection log."""
202
if self._controldir.control_transport.has(self._filename):
203
revlog = self._open_for_read()
205
(revid, status) = line.split()
206
self._items.append((revid, status.decode('ascii')))
209
"""Save the bisection log."""
211
(b"%s %s\n" % (revid, status.encode('ascii')))
212
for (revid, status) in self._items)
214
self._controldir.control_transport.put_bytes(
215
self._filename, contents)
217
sys.stdout.write(contents)
220
"""Report whether we've found the right revision."""
221
return len(self._items) > 0 and self._items[-1][1] == "done"
223
def set_status_from_revspec(self, revspec, status):
224
"""Set the bisection status for the revision in revspec."""
226
revid = revspec[0].in_history(self._branch).rev_id
227
self._set_status(revid, status)
229
def set_current(self, status):
230
"""Set the current revision to the given bisection status."""
231
self._set_status(self._current.get_current_revid(), status)
233
def is_merge_point(self, revid):
234
return len(self.get_parent_revids(revid)) > 1
236
def get_parent_revids(self, revid):
237
repo = self._branch.repository
238
with repo.lock_read():
239
retval = repo.get_parent_map([revid]).get(revid, None)
242
def bisect(self, outf):
243
"""Using the current revision's status, do a bisection."""
244
self._find_range_and_middle()
245
# If we've found the "final" revision, check for a
247
while ((self._middle_revid == self._high_revid or
248
self._middle_revid == self._low_revid) and
249
self.is_merge_point(self._middle_revid)):
250
for parent in self.get_parent_revids(self._middle_revid):
251
if parent == self._low_revid:
254
self._find_range_and_middle(parent)
256
self._switch_wc_to_revno(self._middle_revid, outf)
257
if self._middle_revid == self._high_revid or \
258
self._middle_revid == self._low_revid:
259
self.set_current("done")
262
class cmd_bisect(Command):
263
"""Find an interesting commit using a binary search.
265
Bisecting, in a nutshell, is a way to find the commit at which
266
some testable change was made, such as the introduction of a bug
267
or feature. By identifying a version which did not have the
268
interesting change and a later version which did, a developer
269
can test for the presence of the change at various points in
270
the history, eventually ending up at the precise commit when
271
the change was first introduced.
273
This command uses subcommands to implement the search, each
274
of which changes the state of the bisection. The
278
Start a bisect, possibly clearing out a previous bisect.
280
brz bisect yes [-r rev]
281
The specified revision (or the current revision, if not given)
282
has the characteristic we're looking for,
284
brz bisect no [-r rev]
285
The specified revision (or the current revision, if not given)
286
does not have the characteristic we're looking for,
288
brz bisect move -r rev
289
Switch to a different revision manually. Use if the bisect
290
algorithm chooses a revision that is not suitable. Try to
291
move as little as possible.
294
Clear out a bisection in progress.
296
brz bisect log [-o file]
297
Output a log of the current bisection to standard output, or
298
to the specified file.
300
brz bisect replay <logfile>
301
Replay a previously-saved bisect log, forgetting any bisection
302
that might be in progress.
304
brz bisect run <script>
305
Bisect automatically using <script> to determine 'yes' or 'no'.
306
<script> should exit with:
308
125 for unknown (like build failed so we could not test)
312
takes_args = ['subcommand', 'args*']
313
takes_options = [Option('output', short_name='o',
314
help='Write log to this file.', type=str),
315
'revision', 'directory']
317
def _check(self, controldir):
318
"""Check preconditions for most operations to work."""
319
if not controldir.control_transport.has(BISECT_INFO_PATH):
320
raise BzrCommandError("No bisection in progress.")
322
def _set_state(self, controldir, revspec, state):
323
"""Set the state of the given revspec and bisecting.
325
Returns boolean indicating if bisection is done."""
326
bisect_log = BisectLog(controldir)
327
if bisect_log.is_done():
328
note("No further bisection is possible.\n")
329
bisect_log._current.show_rev_log(outf=self.outf)
333
bisect_log.set_status_from_revspec(revspec, state)
335
bisect_log.set_current(state)
336
bisect_log.bisect(self.outf)
340
def run(self, subcommand, args_list, directory='.', revision=None,
342
"""Handle the bisect command."""
345
if subcommand in ('yes', 'no', 'move') and revision:
347
elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
348
log_fn = args_list[0]
349
elif subcommand in ('move', ) and not revision:
350
raise BzrCommandError(
351
"The 'bisect move' command requires a revision.")
352
elif subcommand in ('run', ):
353
run_script = args_list[0]
354
elif args_list or revision:
355
raise BzrCommandError(
356
"Improper arguments to bisect " + subcommand)
358
controldir, _ = ControlDir.open_containing(directory)
361
if subcommand == "start":
362
self.start(controldir)
363
elif subcommand == "yes":
364
self.yes(controldir, revision)
365
elif subcommand == "no":
366
self.no(controldir, revision)
367
elif subcommand == "move":
368
self.move(controldir, revision)
369
elif subcommand == "reset":
370
self.reset(controldir)
371
elif subcommand == "log":
372
self.log(controldir, output)
373
elif subcommand == "replay":
374
self.replay(controldir, log_fn)
375
elif subcommand == "run":
376
self.run_bisect(controldir, run_script)
378
raise BzrCommandError(
379
"Unknown bisect command: " + subcommand)
381
def reset(self, controldir):
382
"""Reset the bisect state to no state."""
383
self._check(controldir)
384
BisectCurrent(controldir).reset()
385
controldir.control_transport.delete(BISECT_INFO_PATH)
387
def start(self, controldir):
388
"""Reset the bisect state, then prepare for a new bisection."""
389
if controldir.control_transport.has(BISECT_INFO_PATH):
390
BisectCurrent(controldir).reset()
391
controldir.control_transport.delete(BISECT_INFO_PATH)
393
bisect_log = BisectLog(controldir)
394
bisect_log.set_current("start")
397
def yes(self, controldir, revspec):
398
"""Mark that a given revision has the state we're looking for."""
399
self._set_state(controldir, revspec, "yes")
401
def no(self, controldir, revspec):
402
"""Mark a given revision as wrong."""
403
self._set_state(controldir, revspec, "no")
405
def move(self, controldir, revspec):
406
"""Move to a different revision manually."""
407
current = BisectCurrent(controldir)
408
current.switch(revspec)
409
current.show_rev_log(outf=self.outf)
411
def log(self, controldir, filename):
412
"""Write the current bisect log to a file."""
413
self._check(controldir)
414
bisect_log = BisectLog(controldir)
415
bisect_log.change_file_name(filename)
418
def replay(self, controldir, filename):
419
"""Apply the given log file to a clean state, so the state is
420
exactly as it was when the log was saved."""
421
if controldir.control_transport.has(BISECT_INFO_PATH):
422
BisectCurrent(controldir).reset()
423
controldir.control_transport.delete(BISECT_INFO_PATH)
424
bisect_log = BisectLog(controldir, filename)
425
bisect_log.change_file_name(BISECT_INFO_PATH)
428
bisect_log.bisect(self.outf)
430
def run_bisect(self, controldir, script):
432
note("Starting bisect.")
433
self.start(controldir)
436
process = subprocess.Popen(script, shell=True)
438
retcode = process.returncode
440
done = self._set_state(controldir, None, 'yes')
444
done = self._set_state(controldir, None, 'no')