Merge lp:~jelmer/bzr-bisect/lazy into lp:bzr-bisect

Proposed by Jelmer Vernooij
Status: Merged
Merged at revision: 83
Proposed branch: lp:~jelmer/bzr-bisect/lazy
Merge into: lp:bzr-bisect
Diff against target: 1041 lines (+478/-461)
3 files modified
__init__.py (+3/-440)
cmds.py (+455/-0)
tests.py (+20/-21)
To merge this branch: bzr merge lp:~jelmer/bzr-bisect/lazy
Reviewer Review Type Date Requested Status
Martin Pool (community) Approve
Review via email: mp+83319@code.launchpad.net

Description of the change

Lazily load bisect commands.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

I assume you can copy/paste correctly. Do it! :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '__init__.py'
2--- __init__.py 2010-11-05 12:25:01 +0000
3+++ __init__.py 2011-11-24 16:12:23 +0000
4@@ -1,4 +1,4 @@
5-# Copyright (C) 2006-2010 Canonical Ltd
6+# Copyright (C) 2006-2011 Canonical Ltd
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10@@ -16,448 +16,11 @@
11
12 """Support for git-style bisection."""
13
14-import sys
15-import os
16-import bzrlib.bzrdir
17-from bzrlib.commands import Command, register_command
18-from bzrlib.errors import BzrCommandError
19-from bzrlib.option import Option
20-from bzrlib.trace import note
21+from bzrlib.commands import plugin_cmds
22
23 from meta import *
24
25-bisect_info_path = ".bzr/bisect"
26-bisect_rev_path = ".bzr/bisect_revid"
27-
28-
29-class BisectCurrent(object):
30- """Bisect class for managing the current revision."""
31-
32- def __init__(self, filename = bisect_rev_path):
33- self._filename = filename
34- self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing(".")[0]
35- self._bzrbranch = self._bzrdir.open_branch()
36- if os.path.exists(filename):
37- revid_file = open(filename)
38- self._revid = revid_file.read().strip()
39- revid_file.close()
40- else:
41- self._revid = self._bzrbranch.last_revision()
42-
43- def _save(self):
44- """Save the current revision."""
45-
46- revid_file = open(self._filename, "w")
47- revid_file.write(self._revid + "\n")
48- revid_file.close()
49-
50- def get_current_revid(self):
51- """Return the current revision id."""
52- return self._revid
53-
54- def get_current_revno(self):
55- """Return the current revision number as a tuple."""
56- revdict = self._bzrbranch.get_revision_id_to_revno_map()
57- return revdict[self.get_current_revid()]
58-
59- def get_parent_revids(self):
60- """Return the IDs of the current revision's predecessors."""
61- repo = self._bzrbranch.repository
62- repo.lock_read()
63- retval = repo.get_parent_map([self._revid]).get(self._revid, None)
64- repo.unlock()
65- return retval
66-
67- def is_merge_point(self):
68- """Is the current revision a merge point?"""
69- return len(self.get_parent_revids()) > 1
70-
71- def show_rev_log(self, out = sys.stdout):
72- """Write the current revision's log entry to a file."""
73- rev = self._bzrbranch.repository.get_revision(self._revid)
74- revno = ".".join([str(x) for x in self.get_current_revno()])
75- out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
76- rev.message))
77-
78- def switch(self, revid):
79- """Switch the current revision to the given revid."""
80- working = self._bzrdir.open_workingtree()
81- if isinstance(revid, int):
82- revid = self._bzrbranch.get_rev_id(revid)
83- elif isinstance(revid, list):
84- revid = revid[0].in_history(working.branch).rev_id
85- working.revert(None, working.branch.repository.revision_tree(revid),
86- False)
87- self._revid = revid
88- self._save()
89-
90- def reset(self):
91- """Revert bisection, setting the working tree to normal."""
92- working = self._bzrdir.open_workingtree()
93- last_rev = working.branch.last_revision()
94- rev_tree = working.branch.repository.revision_tree(last_rev)
95- working.revert(None, rev_tree, False)
96- if os.path.exists(bisect_rev_path):
97- os.unlink(bisect_rev_path)
98-
99-
100-class BisectLog(object):
101- """Bisect log file handler."""
102-
103- def __init__(self, filename = bisect_info_path):
104- self._items = []
105- self._current = BisectCurrent()
106- self._bzrdir = None
107- self._high_revid = None
108- self._low_revid = None
109- self._middle_revid = None
110- self._filename = filename
111- self.load()
112-
113- def _open_for_read(self):
114- """Open log file for reading."""
115- if self._filename:
116- return open(self._filename)
117- else:
118- return sys.stdin
119-
120- def _open_for_write(self):
121- """Open log file for writing."""
122- if self._filename:
123- return open(self._filename, "w")
124- else:
125- return sys.stdout
126-
127- def _load_bzr_tree(self):
128- """Load bzr information."""
129- if not self._bzrdir:
130- self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing('.')[0]
131- self._bzrbranch = self._bzrdir.open_branch()
132-
133- def _find_range_and_middle(self, branch_last_rev = None):
134- """Find the current revision range, and the midpoint."""
135- self._load_bzr_tree()
136- self._middle_revid = None
137-
138- if not branch_last_rev:
139- last_revid = self._bzrbranch.last_revision()
140- else:
141- last_revid = branch_last_rev
142-
143- repo = self._bzrbranch.repository
144- repo.lock_read()
145- try:
146- rev_sequence = repo.iter_reverse_revision_history(last_revid)
147- high_revid = None
148- low_revid = None
149- between_revs = []
150- for revision in rev_sequence:
151- between_revs.insert(0, revision)
152- matches = [x[1] for x in self._items
153- if x[0] == revision and x[1] in ('yes', 'no')]
154- if not matches:
155- continue
156- if len(matches) > 1:
157- raise RuntimeError("revision %s duplicated" % revision)
158- if matches[0] == "yes":
159- high_revid = revision
160- between_revs = []
161- elif matches[0] == "no":
162- low_revid = revision
163- del between_revs[0]
164- break
165-
166- if not high_revid:
167- high_revid = last_revid
168- if not low_revid:
169- low_revid = self._bzrbranch.get_rev_id(1)
170- finally:
171- repo.unlock()
172-
173- # The spread must include the high revision, to bias
174- # odd numbers of intervening revisions towards the high
175- # side.
176-
177- spread = len(between_revs) + 1
178- if spread < 2:
179- middle_index = 0
180- else:
181- middle_index = (spread / 2) - 1
182-
183- if len(between_revs) > 0:
184- self._middle_revid = between_revs[middle_index]
185- else:
186- self._middle_revid = high_revid
187-
188- self._high_revid = high_revid
189- self._low_revid = low_revid
190-
191- def _switch_wc_to_revno(self, revno, outf):
192- """Move the working tree to the given revno."""
193- self._current.switch(revno)
194- self._current.show_rev_log(out=outf)
195-
196- def _set_status(self, revid, status):
197- """Set the bisect status for the given revid."""
198- if not self.is_done():
199- if status != "done" and revid in [x[0] for x in self._items
200- if x[1] in ['yes', 'no']]:
201- raise RuntimeError("attempting to add revid %s twice" % revid)
202- self._items.append((revid, status))
203-
204- def change_file_name(self, filename):
205- """Switch log files."""
206- self._filename = filename
207-
208- def load(self):
209- """Load the bisection log."""
210- self._items = []
211- if os.path.exists(self._filename):
212- revlog = self._open_for_read()
213- for line in revlog:
214- (revid, status) = line.split()
215- self._items.append((revid, status))
216-
217- def save(self):
218- """Save the bisection log."""
219- revlog = self._open_for_write()
220- for (revid, status) in self._items:
221- revlog.write("%s %s\n" % (revid, status))
222-
223- def is_done(self):
224- """Report whether we've found the right revision."""
225- return len(self._items) > 0 and self._items[-1][1] == "done"
226-
227- def set_status_from_revspec(self, revspec, status):
228- """Set the bisection status for the revision in revspec."""
229- self._load_bzr_tree()
230- revid = revspec[0].in_history(self._bzrbranch).rev_id
231- self._set_status(revid, status)
232-
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)
236-
237- def is_merge_point(self, revid):
238- return len(self.get_parent_revids(revid)) > 1
239-
240- def get_parent_revids(self, revid):
241- repo = self._bzrbranch.repository
242- repo.lock_read()
243- try:
244- retval = repo.get_parent_map([revid]).get(revid, None)
245- finally:
246- repo.unlock()
247- return retval
248-
249- def bisect(self, outf):
250- """Using the current revision's status, do a bisection."""
251- self._find_range_and_middle()
252- # If we've found the "final" revision, check for a
253- # merge point.
254- while ((self._middle_revid == self._high_revid
255- or self._middle_revid == self._low_revid)
256- and self.is_merge_point(self._middle_revid)):
257- for parent in self.get_parent_revids(self._middle_revid):
258- if parent == self._low_revid:
259- continue
260- else:
261- self._find_range_and_middle(parent)
262- break
263- self._switch_wc_to_revno(self._middle_revid, outf)
264- if self._middle_revid == self._high_revid or \
265- self._middle_revid == self._low_revid:
266- self.set_current("done")
267-
268-
269-class cmd_bisect(Command):
270- """Find an interesting commit using a binary search.
271-
272- Bisecting, in a nutshell, is a way to find the commit at which
273- some testable change was made, such as the introduction of a bug
274- or feature. By identifying a version which did not have the
275- interesting change and a later version which did, a developer
276- can test for the presence of the change at various points in
277- the history, eventually ending up at the precise commit when
278- the change was first introduced.
279-
280- This command uses subcommands to implement the search, each
281- of which changes the state of the bisection. The
282- subcommands are:
283-
284- bzr bisect start
285- Start a bisect, possibly clearing out a previous bisect.
286-
287- bzr bisect yes [-r rev]
288- The specified revision (or the current revision, if not given)
289- has the characteristic we're looking for,
290-
291- bzr bisect no [-r rev]
292- The specified revision (or the current revision, if not given)
293- does not have the characteristic we're looking for,
294-
295- bzr bisect move -r rev
296- Switch to a different revision manually. Use if the bisect
297- algorithm chooses a revision that is not suitable. Try to
298- move as little as possible.
299-
300- bzr bisect reset
301- Clear out a bisection in progress.
302-
303- bzr bisect log [-o file]
304- Output a log of the current bisection to standard output, or
305- to the specified file.
306-
307- bzr bisect replay <logfile>
308- Replay a previously-saved bisect log, forgetting any bisection
309- that might be in progress.
310-
311- bzr bisect run <script>
312- Bisect automatically using <script> to determine 'yes' or 'no'.
313- <script> should exit with:
314- 0 for yes
315- 125 for unknown (like build failed so we could not test)
316- anything else for no
317- """
318-
319- takes_args = ['subcommand', 'args*']
320- takes_options = [Option('output', short_name='o',
321- help='Write log to this file.', type=unicode),
322- 'revision']
323-
324- def _check(self):
325- """Check preconditions for most operations to work."""
326- if not os.path.exists(bisect_info_path):
327- raise BzrCommandError("No bisection in progress.")
328-
329- def _set_state(self, revspec, state):
330- """Set the state of the given revspec and bisecting.
331-
332- Returns boolean indicating if bisection is done."""
333- bisect_log = BisectLog()
334- if bisect_log.is_done():
335- note("No further bisection is possible.\n")
336- bisect_log._current.show_rev_log(self.outf)
337- return True
338-
339- if revspec:
340- bisect_log.set_status_from_revspec(revspec, state)
341- else:
342- bisect_log.set_current(state)
343- bisect_log.bisect(self.outf)
344- bisect_log.save()
345- return False
346-
347- def run(self, subcommand, args_list, revision=None, output=None):
348- """Handle the bisect command."""
349-
350- log_fn = None
351- if subcommand in ('yes', 'no', 'move') and revision:
352- pass
353- elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
354- log_fn = args_list[0]
355- elif subcommand in ('move', ) and not revision:
356- raise BzrCommandError(
357- "The 'bisect move' command requires a revision.")
358- elif subcommand in ('run', ):
359- run_script = args_list[0]
360- elif args_list or revision:
361- raise BzrCommandError(
362- "Improper arguments to bisect " + subcommand)
363-
364- # Dispatch.
365-
366- if subcommand == "start":
367- self.start()
368- elif subcommand == "yes":
369- self.yes(revision)
370- elif subcommand == "no":
371- self.no(revision)
372- elif subcommand == "move":
373- self.move(revision)
374- elif subcommand == "reset":
375- self.reset()
376- elif subcommand == "log":
377- self.log(output)
378- elif subcommand == "replay":
379- self.replay(log_fn)
380- elif subcommand == "run":
381- self.run_bisect(run_script)
382- else:
383- raise BzrCommandError(
384- "Unknown bisect command: " + subcommand)
385-
386- def reset(self):
387- """Reset the bisect state to no state."""
388- self._check()
389- BisectCurrent().reset()
390- os.unlink(bisect_info_path)
391-
392- def start(self):
393- """Reset the bisect state, then prepare for a new bisection."""
394- if os.path.exists(bisect_info_path):
395- BisectCurrent().reset()
396- os.unlink(bisect_info_path)
397-
398- bisect_log = BisectLog()
399- bisect_log.set_current("start")
400- bisect_log.save()
401-
402- def yes(self, revspec):
403- """Mark that a given revision has the state we're looking for."""
404- self._set_state(revspec, "yes")
405-
406- def no(self, revspec):
407- """Mark that a given revision does not have the state we're looking for."""
408- self._set_state(revspec, "no")
409-
410- def move(self, revspec):
411- """Move to a different revision manually."""
412- current = BisectCurrent()
413- current.switch(revspec)
414- current.show_rev_log(out=self.outf)
415-
416- def log(self, filename):
417- """Write the current bisect log to a file."""
418- self._check()
419- bisect_log = BisectLog()
420- bisect_log.change_file_name(filename)
421- bisect_log.save()
422-
423- def replay(self, filename):
424- """Apply the given log file to a clean state, so the state is
425- exactly as it was when the log was saved."""
426- if os.path.exists(bisect_info_path):
427- BisectCurrent().reset()
428- os.unlink(bisect_info_path)
429- bisect_log = BisectLog(filename)
430- bisect_log.change_file_name(bisect_info_path)
431- bisect_log.save()
432-
433- bisect_log.bisect(self.outf)
434-
435- def run_bisect(self, script):
436- import subprocess
437- note("Starting bisect.")
438- self.start()
439- while True:
440- try:
441- process = subprocess.Popen(script, shell=True)
442- process.wait()
443- retcode = process.returncode
444- if retcode == 0:
445- done = self._set_state(None, 'yes')
446- elif retcode == 125:
447- break
448- else:
449- done = self._set_state(None, 'no')
450- if done:
451- break
452- except RuntimeError:
453- break
454-
455-register_command(cmd_bisect)
456-
457+plugin_cmds.register_lazy('cmd_bisect', [], 'bzrlib.plugins.bisect.cmds')
458
459 def load_tests(basic_tests, module, loader):
460 testmod_names = [
461
462=== added file 'cmds.py'
463--- cmds.py 1970-01-01 00:00:00 +0000
464+++ cmds.py 2011-11-24 16:12:23 +0000
465@@ -0,0 +1,455 @@
466+# Copyright (C) 2006-2011 Canonical Ltd
467+#
468+# This program is free software; you can redistribute it and/or modify
469+# it under the terms of the GNU General Public License as published by
470+# the Free Software Foundation; either version 2 of the License, or
471+# (at your option) any later version.
472+#
473+# This program is distributed in the hope that it will be useful,
474+# but WITHOUT ANY WARRANTY; without even the implied warranty of
475+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
476+# GNU General Public License for more details.
477+#
478+# You should have received a copy of the GNU General Public License
479+# along with this program; if not, write to the Free Software
480+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
481+
482+"""bisect command implementations."""
483+
484+import sys
485+import os
486+import bzrlib.bzrdir
487+from bzrlib.commands import Command
488+from bzrlib.errors import BzrCommandError
489+from bzrlib.option import Option
490+from bzrlib.trace import note
491+
492+bisect_info_path = ".bzr/bisect"
493+bisect_rev_path = ".bzr/bisect_revid"
494+
495+
496+class BisectCurrent(object):
497+ """Bisect class for managing the current revision."""
498+
499+ def __init__(self, filename = bisect_rev_path):
500+ self._filename = filename
501+ self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing(".")[0]
502+ self._bzrbranch = self._bzrdir.open_branch()
503+ if os.path.exists(filename):
504+ revid_file = open(filename)
505+ self._revid = revid_file.read().strip()
506+ revid_file.close()
507+ else:
508+ self._revid = self._bzrbranch.last_revision()
509+
510+ def _save(self):
511+ """Save the current revision."""
512+
513+ revid_file = open(self._filename, "w")
514+ revid_file.write(self._revid + "\n")
515+ revid_file.close()
516+
517+ def get_current_revid(self):
518+ """Return the current revision id."""
519+ return self._revid
520+
521+ def get_current_revno(self):
522+ """Return the current revision number as a tuple."""
523+ revdict = self._bzrbranch.get_revision_id_to_revno_map()
524+ return revdict[self.get_current_revid()]
525+
526+ def get_parent_revids(self):
527+ """Return the IDs of the current revision's predecessors."""
528+ repo = self._bzrbranch.repository
529+ repo.lock_read()
530+ retval = repo.get_parent_map([self._revid]).get(self._revid, None)
531+ repo.unlock()
532+ return retval
533+
534+ def is_merge_point(self):
535+ """Is the current revision a merge point?"""
536+ return len(self.get_parent_revids()) > 1
537+
538+ def show_rev_log(self, out = sys.stdout):
539+ """Write the current revision's log entry to a file."""
540+ rev = self._bzrbranch.repository.get_revision(self._revid)
541+ revno = ".".join([str(x) for x in self.get_current_revno()])
542+ out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id,
543+ rev.message))
544+
545+ def switch(self, revid):
546+ """Switch the current revision to the given revid."""
547+ working = self._bzrdir.open_workingtree()
548+ if isinstance(revid, int):
549+ revid = self._bzrbranch.get_rev_id(revid)
550+ elif isinstance(revid, list):
551+ revid = revid[0].in_history(working.branch).rev_id
552+ working.revert(None, working.branch.repository.revision_tree(revid),
553+ False)
554+ self._revid = revid
555+ self._save()
556+
557+ def reset(self):
558+ """Revert bisection, setting the working tree to normal."""
559+ working = self._bzrdir.open_workingtree()
560+ last_rev = working.branch.last_revision()
561+ rev_tree = working.branch.repository.revision_tree(last_rev)
562+ working.revert(None, rev_tree, False)
563+ if os.path.exists(bisect_rev_path):
564+ os.unlink(bisect_rev_path)
565+
566+
567+class BisectLog(object):
568+ """Bisect log file handler."""
569+
570+ def __init__(self, filename = bisect_info_path):
571+ self._items = []
572+ self._current = BisectCurrent()
573+ self._bzrdir = None
574+ self._high_revid = None
575+ self._low_revid = None
576+ self._middle_revid = None
577+ self._filename = filename
578+ self.load()
579+
580+ def _open_for_read(self):
581+ """Open log file for reading."""
582+ if self._filename:
583+ return open(self._filename)
584+ else:
585+ return sys.stdin
586+
587+ def _open_for_write(self):
588+ """Open log file for writing."""
589+ if self._filename:
590+ return open(self._filename, "w")
591+ else:
592+ return sys.stdout
593+
594+ def _load_bzr_tree(self):
595+ """Load bzr information."""
596+ if not self._bzrdir:
597+ self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing('.')[0]
598+ self._bzrbranch = self._bzrdir.open_branch()
599+
600+ def _find_range_and_middle(self, branch_last_rev = None):
601+ """Find the current revision range, and the midpoint."""
602+ self._load_bzr_tree()
603+ self._middle_revid = None
604+
605+ if not branch_last_rev:
606+ last_revid = self._bzrbranch.last_revision()
607+ else:
608+ last_revid = branch_last_rev
609+
610+ repo = self._bzrbranch.repository
611+ repo.lock_read()
612+ try:
613+ rev_sequence = repo.iter_reverse_revision_history(last_revid)
614+ high_revid = None
615+ low_revid = None
616+ between_revs = []
617+ for revision in rev_sequence:
618+ between_revs.insert(0, revision)
619+ matches = [x[1] for x in self._items
620+ if x[0] == revision and x[1] in ('yes', 'no')]
621+ if not matches:
622+ continue
623+ if len(matches) > 1:
624+ raise RuntimeError("revision %s duplicated" % revision)
625+ if matches[0] == "yes":
626+ high_revid = revision
627+ between_revs = []
628+ elif matches[0] == "no":
629+ low_revid = revision
630+ del between_revs[0]
631+ break
632+
633+ if not high_revid:
634+ high_revid = last_revid
635+ if not low_revid:
636+ low_revid = self._bzrbranch.get_rev_id(1)
637+ finally:
638+ repo.unlock()
639+
640+ # The spread must include the high revision, to bias
641+ # odd numbers of intervening revisions towards the high
642+ # side.
643+
644+ spread = len(between_revs) + 1
645+ if spread < 2:
646+ middle_index = 0
647+ else:
648+ middle_index = (spread / 2) - 1
649+
650+ if len(between_revs) > 0:
651+ self._middle_revid = between_revs[middle_index]
652+ else:
653+ self._middle_revid = high_revid
654+
655+ self._high_revid = high_revid
656+ self._low_revid = low_revid
657+
658+ def _switch_wc_to_revno(self, revno, outf):
659+ """Move the working tree to the given revno."""
660+ self._current.switch(revno)
661+ self._current.show_rev_log(out=outf)
662+
663+ def _set_status(self, revid, status):
664+ """Set the bisect status for the given revid."""
665+ if not self.is_done():
666+ if status != "done" and revid in [x[0] for x in self._items
667+ if x[1] in ['yes', 'no']]:
668+ raise RuntimeError("attempting to add revid %s twice" % revid)
669+ self._items.append((revid, status))
670+
671+ def change_file_name(self, filename):
672+ """Switch log files."""
673+ self._filename = filename
674+
675+ def load(self):
676+ """Load the bisection log."""
677+ self._items = []
678+ if os.path.exists(self._filename):
679+ revlog = self._open_for_read()
680+ for line in revlog:
681+ (revid, status) = line.split()
682+ self._items.append((revid, status))
683+
684+ def save(self):
685+ """Save the bisection log."""
686+ revlog = self._open_for_write()
687+ for (revid, status) in self._items:
688+ revlog.write("%s %s\n" % (revid, status))
689+
690+ def is_done(self):
691+ """Report whether we've found the right revision."""
692+ return len(self._items) > 0 and self._items[-1][1] == "done"
693+
694+ def set_status_from_revspec(self, revspec, status):
695+ """Set the bisection status for the revision in revspec."""
696+ self._load_bzr_tree()
697+ revid = revspec[0].in_history(self._bzrbranch).rev_id
698+ self._set_status(revid, status)
699+
700+ def set_current(self, status):
701+ """Set the current revision to the given bisection status."""
702+ self._set_status(self._current.get_current_revid(), status)
703+
704+ def is_merge_point(self, revid):
705+ return len(self.get_parent_revids(revid)) > 1
706+
707+ def get_parent_revids(self, revid):
708+ repo = self._bzrbranch.repository
709+ repo.lock_read()
710+ try:
711+ retval = repo.get_parent_map([revid]).get(revid, None)
712+ finally:
713+ repo.unlock()
714+ return retval
715+
716+ def bisect(self, outf):
717+ """Using the current revision's status, do a bisection."""
718+ self._find_range_and_middle()
719+ # If we've found the "final" revision, check for a
720+ # merge point.
721+ while ((self._middle_revid == self._high_revid
722+ or self._middle_revid == self._low_revid)
723+ and self.is_merge_point(self._middle_revid)):
724+ for parent in self.get_parent_revids(self._middle_revid):
725+ if parent == self._low_revid:
726+ continue
727+ else:
728+ self._find_range_and_middle(parent)
729+ break
730+ self._switch_wc_to_revno(self._middle_revid, outf)
731+ if self._middle_revid == self._high_revid or \
732+ self._middle_revid == self._low_revid:
733+ self.set_current("done")
734+
735+
736+class cmd_bisect(Command):
737+ """Find an interesting commit using a binary search.
738+
739+ Bisecting, in a nutshell, is a way to find the commit at which
740+ some testable change was made, such as the introduction of a bug
741+ or feature. By identifying a version which did not have the
742+ interesting change and a later version which did, a developer
743+ can test for the presence of the change at various points in
744+ the history, eventually ending up at the precise commit when
745+ the change was first introduced.
746+
747+ This command uses subcommands to implement the search, each
748+ of which changes the state of the bisection. The
749+ subcommands are:
750+
751+ bzr bisect start
752+ Start a bisect, possibly clearing out a previous bisect.
753+
754+ bzr bisect yes [-r rev]
755+ The specified revision (or the current revision, if not given)
756+ has the characteristic we're looking for,
757+
758+ bzr bisect no [-r rev]
759+ The specified revision (or the current revision, if not given)
760+ does not have the characteristic we're looking for,
761+
762+ bzr bisect move -r rev
763+ Switch to a different revision manually. Use if the bisect
764+ algorithm chooses a revision that is not suitable. Try to
765+ move as little as possible.
766+
767+ bzr bisect reset
768+ Clear out a bisection in progress.
769+
770+ bzr bisect log [-o file]
771+ Output a log of the current bisection to standard output, or
772+ to the specified file.
773+
774+ bzr bisect replay <logfile>
775+ Replay a previously-saved bisect log, forgetting any bisection
776+ that might be in progress.
777+
778+ bzr bisect run <script>
779+ Bisect automatically using <script> to determine 'yes' or 'no'.
780+ <script> should exit with:
781+ 0 for yes
782+ 125 for unknown (like build failed so we could not test)
783+ anything else for no
784+ """
785+
786+ takes_args = ['subcommand', 'args*']
787+ takes_options = [Option('output', short_name='o',
788+ help='Write log to this file.', type=unicode),
789+ 'revision']
790+
791+ def _check(self):
792+ """Check preconditions for most operations to work."""
793+ if not os.path.exists(bisect_info_path):
794+ raise BzrCommandError("No bisection in progress.")
795+
796+ def _set_state(self, revspec, state):
797+ """Set the state of the given revspec and bisecting.
798+
799+ Returns boolean indicating if bisection is done."""
800+ bisect_log = BisectLog()
801+ if bisect_log.is_done():
802+ note("No further bisection is possible.\n")
803+ bisect_log._current.show_rev_log(self.outf)
804+ return True
805+
806+ if revspec:
807+ bisect_log.set_status_from_revspec(revspec, state)
808+ else:
809+ bisect_log.set_current(state)
810+ bisect_log.bisect(self.outf)
811+ bisect_log.save()
812+ return False
813+
814+ def run(self, subcommand, args_list, revision=None, output=None):
815+ """Handle the bisect command."""
816+
817+ log_fn = None
818+ if subcommand in ('yes', 'no', 'move') and revision:
819+ pass
820+ elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
821+ log_fn = args_list[0]
822+ elif subcommand in ('move', ) and not revision:
823+ raise BzrCommandError(
824+ "The 'bisect move' command requires a revision.")
825+ elif subcommand in ('run', ):
826+ run_script = args_list[0]
827+ elif args_list or revision:
828+ raise BzrCommandError(
829+ "Improper arguments to bisect " + subcommand)
830+
831+ # Dispatch.
832+
833+ if subcommand == "start":
834+ self.start()
835+ elif subcommand == "yes":
836+ self.yes(revision)
837+ elif subcommand == "no":
838+ self.no(revision)
839+ elif subcommand == "move":
840+ self.move(revision)
841+ elif subcommand == "reset":
842+ self.reset()
843+ elif subcommand == "log":
844+ self.log(output)
845+ elif subcommand == "replay":
846+ self.replay(log_fn)
847+ elif subcommand == "run":
848+ self.run_bisect(run_script)
849+ else:
850+ raise BzrCommandError(
851+ "Unknown bisect command: " + subcommand)
852+
853+ def reset(self):
854+ """Reset the bisect state to no state."""
855+ self._check()
856+ BisectCurrent().reset()
857+ os.unlink(bisect_info_path)
858+
859+ def start(self):
860+ """Reset the bisect state, then prepare for a new bisection."""
861+ if os.path.exists(bisect_info_path):
862+ BisectCurrent().reset()
863+ os.unlink(bisect_info_path)
864+
865+ bisect_log = BisectLog()
866+ bisect_log.set_current("start")
867+ bisect_log.save()
868+
869+ def yes(self, revspec):
870+ """Mark that a given revision has the state we're looking for."""
871+ self._set_state(revspec, "yes")
872+
873+ def no(self, revspec):
874+ """Mark that a given revision does not have the state we're looking for."""
875+ self._set_state(revspec, "no")
876+
877+ def move(self, revspec):
878+ """Move to a different revision manually."""
879+ current = BisectCurrent()
880+ current.switch(revspec)
881+ current.show_rev_log(out=self.outf)
882+
883+ def log(self, filename):
884+ """Write the current bisect log to a file."""
885+ self._check()
886+ bisect_log = BisectLog()
887+ bisect_log.change_file_name(filename)
888+ bisect_log.save()
889+
890+ def replay(self, filename):
891+ """Apply the given log file to a clean state, so the state is
892+ exactly as it was when the log was saved."""
893+ if os.path.exists(bisect_info_path):
894+ BisectCurrent().reset()
895+ os.unlink(bisect_info_path)
896+ bisect_log = BisectLog(filename)
897+ bisect_log.change_file_name(bisect_info_path)
898+ bisect_log.save()
899+
900+ bisect_log.bisect(self.outf)
901+
902+ def run_bisect(self, script):
903+ import subprocess
904+ note("Starting bisect.")
905+ self.start()
906+ while True:
907+ try:
908+ process = subprocess.Popen(script, shell=True)
909+ process.wait()
910+ retcode = process.returncode
911+ if retcode == 0:
912+ done = self._set_state(None, 'yes')
913+ elif retcode == 125:
914+ break
915+ else:
916+ done = self._set_state(None, 'no')
917+ if done:
918+ break
919+ except RuntimeError:
920+ break
921
922=== modified file 'tests.py'
923--- tests.py 2010-11-05 12:19:42 +0000
924+++ tests.py 2011-11-24 16:12:23 +0000
925@@ -23,16 +23,16 @@
926 import shutil
927
928 import bzrlib
929-import bzrlib.bzrdir
930-import bzrlib.tests
931-import bzrlib.revisionspec
932-import bzrlib.plugins.bisect as bisect
933+from bzrlib.bzrdir import BzrDir
934+from bzrlib.plugins import bisect
935+from bzrlib.plugins.bisect import cmds
936 from bzrlib.tests import (
937 KnownFailure,
938+ TestCaseWithTransport,
939 TestSkipped,
940 )
941
942-class BisectTestCase(bzrlib.tests.TestCaseWithTransport):
943+class BisectTestCase(TestCaseWithTransport):
944 """Test harness specific to the bisect plugin."""
945
946 def assertRevno(self, rev):
947@@ -57,8 +57,7 @@
948 # a branch from version 1 containing three revisions
949 # merged at version 2.
950
951- bzrlib.tests.TestCaseWithTransport.setUp(self)
952-
953+ TestCaseWithTransport.setUp(self)
954
955 self.tree = self.make_branch_and_tree(".")
956
957@@ -74,8 +73,8 @@
958 'test_file_append')))
959 self.tree.commit(message = "add test files")
960
961- bzrlib.bzrdir.BzrDir.open(".").sprout("../temp-clone")
962- clone_bzrdir = bzrlib.bzrdir.BzrDir.open("../temp-clone")
963+ BzrDir.open(".").sprout("../temp-clone")
964+ clone_bzrdir = BzrDir.open("../temp-clone")
965 clone_tree = clone_bzrdir.open_workingtree()
966 for content in ["one dot one", "one dot two", "one dot three"]:
967 test_file = open("../temp-clone/test_file", "w")
968@@ -157,33 +156,33 @@
969 # Not a very good test; just makes sure the code doesn't fail,
970 # not that the output makes any sense.
971 sio = StringIO()
972- bisect.BisectCurrent().show_rev_log(out=sio)
973+ cmds.BisectCurrent().show_rev_log(out=sio)
974
975 def testShowLogSubtree(self):
976 """Test that a subtree's log can be shown."""
977- current = bisect.BisectCurrent()
978+ current = cmds.BisectCurrent()
979 current.switch(self.subtree_rev)
980 sio = StringIO()
981 current.show_rev_log(out=sio)
982
983 def testSwitchVersions(self):
984 """Test switching versions."""
985- current = bisect.BisectCurrent()
986+ current = cmds.BisectCurrent()
987 self.assertRevno(5)
988 current.switch(4)
989 self.assertRevno(4)
990
991 def testReset(self):
992 """Test resetting the working tree to a non-bisected state."""
993- current = bisect.BisectCurrent()
994+ current = cmds.BisectCurrent()
995 current.switch(4)
996 current.reset()
997 self.assertRevno(5)
998- assert not os.path.exists(bisect.bisect_rev_path)
999+ assert not os.path.exists(cmds.bisect_rev_path)
1000
1001 def testIsMergePoint(self):
1002 """Test merge point detection."""
1003- current = bisect.BisectCurrent()
1004+ current = cmds.BisectCurrent()
1005 self.assertRevno(5)
1006 assert not current.is_merge_point()
1007 current.switch(2)
1008@@ -195,17 +194,17 @@
1009
1010 def testCreateBlank(self):
1011 """Test creation of new log."""
1012- bisect_log = bisect.BisectLog()
1013+ bisect_log = cmds.BisectLog()
1014 bisect_log.save()
1015- assert os.path.exists(bisect.bisect_info_path)
1016+ assert os.path.exists(cmds.bisect_info_path)
1017
1018 def testLoad(self):
1019 """Test loading a log."""
1020- preloaded_log = open(bisect.bisect_info_path, "w")
1021+ preloaded_log = open(cmds.bisect_info_path, "w")
1022 preloaded_log.write("rev1 yes\nrev2 no\nrev3 yes\n")
1023 preloaded_log.close()
1024
1025- bisect_log = bisect.BisectLog()
1026+ bisect_log = cmds.BisectLog()
1027 assert len(bisect_log._items) == 3
1028 assert bisect_log._items[0] == ("rev1", "yes")
1029 assert bisect_log._items[1] == ("rev2", "no")
1030@@ -213,11 +212,11 @@
1031
1032 def testSave(self):
1033 """Test saving the log."""
1034- bisect_log = bisect.BisectLog()
1035+ bisect_log = cmds.BisectLog()
1036 bisect_log._items = [("rev1", "yes"), ("rev2", "no"), ("rev3", "yes")]
1037 bisect_log.save()
1038
1039- logfile = open(bisect.bisect_info_path)
1040+ logfile = open(cmds.bisect_info_path)
1041 assert logfile.read() == "rev1 yes\nrev2 no\nrev3 yes\n"
1042
1043

Subscribers

People subscribed via source and target branches

to all changes: