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 + b"\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(
142
last_revid, (_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.decode('ascii')))
212
"""Save the bisection log."""
214
(b"%s %s\n" % (revid, status.encode('ascii')))
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 or
251
self._middle_revid == self._low_revid) and
252
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,
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 a given revision as wrong."""
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(outf=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')