/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/plugins/bisect/cmds.py

Bundle bisect plugin.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2011 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""bisect command implementations."""
 
18
 
 
19
import sys
 
20
import os
 
21
import bzrlib.bzrdir
 
22
from bzrlib import revision as _mod_revision
 
23
from bzrlib.commands import Command
 
24
from bzrlib.errors import BzrCommandError
 
25
from bzrlib.option import Option
 
26
from bzrlib.trace import note
 
27
 
 
28
bisect_info_path = ".bzr/bisect"
 
29
bisect_rev_path = ".bzr/bisect_revid"
 
30
 
 
31
 
 
32
class BisectCurrent(object):
 
33
    """Bisect class for managing the current revision."""
 
34
 
 
35
    def __init__(self, filename = bisect_rev_path):
 
36
        self._filename = filename
 
37
        self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing(".")[0]
 
38
        self._bzrbranch = self._bzrdir.open_branch()
 
39
        if os.path.exists(filename):
 
40
            revid_file = open(filename)
 
41
            self._revid = revid_file.read().strip()
 
42
            revid_file.close()
 
43
        else:
 
44
            self._revid = self._bzrbranch.last_revision()
 
45
 
 
46
    def _save(self):
 
47
        """Save the current revision."""
 
48
 
 
49
        revid_file = open(self._filename, "w")
 
50
        revid_file.write(self._revid + "\n")
 
51
        revid_file.close()
 
52
 
 
53
    def get_current_revid(self):
 
54
        """Return the current revision id."""
 
55
        return self._revid
 
56
 
 
57
    def get_current_revno(self):
 
58
        """Return the current revision number as a tuple."""
 
59
        revdict = self._bzrbranch.get_revision_id_to_revno_map()
 
60
        return revdict[self.get_current_revid()]
 
61
 
 
62
    def get_parent_revids(self):
 
63
        """Return the IDs of the current revision's predecessors."""
 
64
        repo = self._bzrbranch.repository
 
65
        repo.lock_read()
 
66
        retval = repo.get_parent_map([self._revid]).get(self._revid, None)
 
67
        repo.unlock()
 
68
        return retval
 
69
 
 
70
    def is_merge_point(self):
 
71
        """Is the current revision a merge point?"""
 
72
        return len(self.get_parent_revids()) > 1
 
73
 
 
74
    def show_rev_log(self, out = sys.stdout):
 
75
        """Write the current revision's log entry to a file."""
 
76
        rev = self._bzrbranch.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,
 
79
                                                  rev.message))
 
80
 
 
81
    def switch(self, revid):
 
82
        """Switch the current revision to the given revid."""
 
83
        working = self._bzrdir.open_workingtree()
 
84
        if isinstance(revid, int):
 
85
            revid = self._bzrbranch.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),
 
89
                       False)
 
90
        self._revid = revid
 
91
        self._save()
 
92
 
 
93
    def reset(self):
 
94
        """Revert bisection, setting the working tree to normal."""
 
95
        working = self._bzrdir.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 os.path.exists(bisect_rev_path):
 
100
            os.unlink(bisect_rev_path)
 
101
 
 
102
 
 
103
class BisectLog(object):
 
104
    """Bisect log file handler."""
 
105
 
 
106
    def __init__(self, filename = bisect_info_path):
 
107
        self._items = []
 
108
        self._current = BisectCurrent()
 
109
        self._bzrdir = None
 
110
        self._high_revid = None
 
111
        self._low_revid = None
 
112
        self._middle_revid = None
 
113
        self._filename = filename
 
114
        self.load()
 
115
 
 
116
    def _open_for_read(self):
 
117
        """Open log file for reading."""
 
118
        if self._filename:
 
119
            return open(self._filename)
 
120
        else:
 
121
            return sys.stdin
 
122
 
 
123
    def _open_for_write(self):
 
124
        """Open log file for writing."""
 
125
        if self._filename:
 
126
            return open(self._filename, "w")
 
127
        else:
 
128
            return sys.stdout
 
129
 
 
130
    def _load_bzr_tree(self):
 
131
        """Load bzr information."""
 
132
        if not self._bzrdir:
 
133
            self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing('.')[0]
 
134
            self._bzrbranch = self._bzrdir.open_branch()
 
135
 
 
136
    def _find_range_and_middle(self, branch_last_rev = None):
 
137
        """Find the current revision range, and the midpoint."""
 
138
        self._load_bzr_tree()
 
139
        self._middle_revid = None
 
140
 
 
141
        if not branch_last_rev:
 
142
            last_revid = self._bzrbranch.last_revision()
 
143
        else:
 
144
            last_revid = branch_last_rev
 
145
 
 
146
        repo = self._bzrbranch.repository
 
147
        repo.lock_read()
 
148
        try:
 
149
            graph = repo.get_graph()
 
150
            rev_sequence = graph.iter_lefthand_ancestry(last_revid,
 
151
                (_mod_revision.NULL_REVISION,))
 
152
            high_revid = None
 
153
            low_revid = None
 
154
            between_revs = []
 
155
            for revision in rev_sequence:
 
156
                between_revs.insert(0, revision)
 
157
                matches = [x[1] for x in self._items
 
158
                           if x[0] == revision and x[1] in ('yes', 'no')]
 
159
                if not matches:
 
160
                    continue
 
161
                if len(matches) > 1:
 
162
                    raise RuntimeError("revision %s duplicated" % revision)
 
163
                if matches[0] == "yes":
 
164
                    high_revid = revision
 
165
                    between_revs = []
 
166
                elif matches[0] == "no":
 
167
                    low_revid = revision
 
168
                    del between_revs[0]
 
169
                    break
 
170
 
 
171
            if not high_revid:
 
172
                high_revid = last_revid
 
173
            if not low_revid:
 
174
                low_revid = self._bzrbranch.get_rev_id(1)
 
175
        finally:
 
176
            repo.unlock()
 
177
 
 
178
        # The spread must include the high revision, to bias
 
179
        # odd numbers of intervening revisions towards the high
 
180
        # side.
 
181
 
 
182
        spread = len(between_revs) + 1
 
183
        if spread < 2:
 
184
            middle_index = 0
 
185
        else:
 
186
            middle_index = (spread / 2) - 1
 
187
 
 
188
        if len(between_revs) > 0:
 
189
            self._middle_revid = between_revs[middle_index]
 
190
        else:
 
191
            self._middle_revid = high_revid
 
192
 
 
193
        self._high_revid = high_revid
 
194
        self._low_revid = low_revid
 
195
 
 
196
    def _switch_wc_to_revno(self, revno, outf):
 
197
        """Move the working tree to the given revno."""
 
198
        self._current.switch(revno)
 
199
        self._current.show_rev_log(out=outf)
 
200
 
 
201
    def _set_status(self, revid, status):
 
202
        """Set the bisect status for the given revid."""
 
203
        if not self.is_done():
 
204
            if status != "done" and revid in [x[0] for x in self._items
 
205
                                              if x[1] in ['yes', 'no']]:
 
206
                raise RuntimeError("attempting to add revid %s twice" % revid)
 
207
            self._items.append((revid, status))
 
208
 
 
209
    def change_file_name(self, filename):
 
210
        """Switch log files."""
 
211
        self._filename = filename
 
212
 
 
213
    def load(self):
 
214
        """Load the bisection log."""
 
215
        self._items = []
 
216
        if os.path.exists(self._filename):
 
217
            revlog = self._open_for_read()
 
218
            for line in revlog:
 
219
                (revid, status) = line.split()
 
220
                self._items.append((revid, status))
 
221
 
 
222
    def save(self):
 
223
        """Save the bisection log."""
 
224
        revlog = self._open_for_write()
 
225
        for (revid, status) in self._items:
 
226
            revlog.write("%s %s\n" % (revid, status))
 
227
 
 
228
    def is_done(self):
 
229
        """Report whether we've found the right revision."""
 
230
        return len(self._items) > 0 and self._items[-1][1] == "done"
 
231
 
 
232
    def set_status_from_revspec(self, revspec, status):
 
233
        """Set the bisection status for the revision in revspec."""
 
234
        self._load_bzr_tree()
 
235
        revid = revspec[0].in_history(self._bzrbranch).rev_id
 
236
        self._set_status(revid, status)
 
237
 
 
238
    def set_current(self, status):
 
239
        """Set the current revision to the given bisection status."""
 
240
        self._set_status(self._current.get_current_revid(), status)
 
241
 
 
242
    def is_merge_point(self, revid):
 
243
        return len(self.get_parent_revids(revid)) > 1
 
244
 
 
245
    def get_parent_revids(self, revid):
 
246
        repo = self._bzrbranch.repository
 
247
        repo.lock_read()
 
248
        try:
 
249
            retval = repo.get_parent_map([revid]).get(revid, None)
 
250
        finally:
 
251
            repo.unlock()
 
252
        return retval
 
253
 
 
254
    def bisect(self, outf):
 
255
        """Using the current revision's status, do a bisection."""
 
256
        self._find_range_and_middle()
 
257
        # If we've found the "final" revision, check for a
 
258
        # merge point.
 
259
        while ((self._middle_revid == self._high_revid
 
260
                or self._middle_revid == self._low_revid)
 
261
                and self.is_merge_point(self._middle_revid)):
 
262
            for parent in self.get_parent_revids(self._middle_revid):
 
263
                if parent == self._low_revid:
 
264
                    continue
 
265
                else:
 
266
                    self._find_range_and_middle(parent)
 
267
                    break
 
268
        self._switch_wc_to_revno(self._middle_revid, outf)
 
269
        if self._middle_revid == self._high_revid or \
 
270
           self._middle_revid == self._low_revid:
 
271
            self.set_current("done")
 
272
 
 
273
 
 
274
class cmd_bisect(Command):
 
275
    """Find an interesting commit using a binary search.
 
276
 
 
277
    Bisecting, in a nutshell, is a way to find the commit at which
 
278
    some testable change was made, such as the introduction of a bug
 
279
    or feature.  By identifying a version which did not have the
 
280
    interesting change and a later version which did, a developer
 
281
    can test for the presence of the change at various points in
 
282
    the history, eventually ending up at the precise commit when
 
283
    the change was first introduced.
 
284
 
 
285
    This command uses subcommands to implement the search, each
 
286
    of which changes the state of the bisection.  The
 
287
    subcommands are:
 
288
 
 
289
    bzr bisect start
 
290
        Start a bisect, possibly clearing out a previous bisect.
 
291
 
 
292
    bzr bisect yes [-r rev]
 
293
        The specified revision (or the current revision, if not given)
 
294
        has the characteristic we're looking for,
 
295
 
 
296
    bzr bisect no [-r rev]
 
297
        The specified revision (or the current revision, if not given)
 
298
        does not have the characteristic we're looking for,
 
299
 
 
300
    bzr bisect move -r rev
 
301
        Switch to a different revision manually.  Use if the bisect
 
302
        algorithm chooses a revision that is not suitable.  Try to
 
303
        move as little as possible.
 
304
 
 
305
    bzr bisect reset
 
306
        Clear out a bisection in progress.
 
307
 
 
308
    bzr bisect log [-o file]
 
309
        Output a log of the current bisection to standard output, or
 
310
        to the specified file.
 
311
 
 
312
    bzr bisect replay <logfile>
 
313
        Replay a previously-saved bisect log, forgetting any bisection
 
314
        that might be in progress.
 
315
 
 
316
    bzr bisect run <script>
 
317
        Bisect automatically using <script> to determine 'yes' or 'no'.
 
318
        <script> should exit with:
 
319
           0 for yes
 
320
           125 for unknown (like build failed so we could not test)
 
321
           anything else for no
 
322
    """
 
323
 
 
324
    takes_args = ['subcommand', 'args*']
 
325
    takes_options = [Option('output', short_name='o',
 
326
                            help='Write log to this file.', type=unicode),
 
327
                     'revision']
 
328
 
 
329
    def _check(self):
 
330
        """Check preconditions for most operations to work."""
 
331
        if not os.path.exists(bisect_info_path):
 
332
            raise BzrCommandError("No bisection in progress.")
 
333
 
 
334
    def _set_state(self, revspec, state):
 
335
        """Set the state of the given revspec and bisecting.
 
336
 
 
337
        Returns boolean indicating if bisection is done."""
 
338
        bisect_log = BisectLog()
 
339
        if bisect_log.is_done():
 
340
            note("No further bisection is possible.\n")
 
341
            bisect_log._current.show_rev_log(self.outf)
 
342
            return True
 
343
 
 
344
        if revspec:
 
345
            bisect_log.set_status_from_revspec(revspec, state)
 
346
        else:
 
347
            bisect_log.set_current(state)
 
348
        bisect_log.bisect(self.outf)
 
349
        bisect_log.save()
 
350
        return False
 
351
 
 
352
    def run(self, subcommand, args_list, revision=None, output=None):
 
353
        """Handle the bisect command."""
 
354
 
 
355
        log_fn = None
 
356
        if subcommand in ('yes', 'no', 'move') and revision:
 
357
            pass
 
358
        elif subcommand in ('replay', ) and args_list and len(args_list) == 1:
 
359
            log_fn = args_list[0]
 
360
        elif subcommand in ('move', ) and not revision:
 
361
            raise BzrCommandError(
 
362
                "The 'bisect move' command requires a revision.")
 
363
        elif subcommand in ('run', ):
 
364
            run_script = args_list[0]
 
365
        elif args_list or revision:
 
366
            raise BzrCommandError(
 
367
                "Improper arguments to bisect " + subcommand)
 
368
 
 
369
        # Dispatch.
 
370
 
 
371
        if subcommand == "start":
 
372
            self.start()
 
373
        elif subcommand == "yes":
 
374
            self.yes(revision)
 
375
        elif subcommand == "no":
 
376
            self.no(revision)
 
377
        elif subcommand == "move":
 
378
            self.move(revision)
 
379
        elif subcommand == "reset":
 
380
            self.reset()
 
381
        elif subcommand == "log":
 
382
            self.log(output)
 
383
        elif subcommand == "replay":
 
384
            self.replay(log_fn)
 
385
        elif subcommand == "run":
 
386
            self.run_bisect(run_script)
 
387
        else:
 
388
            raise BzrCommandError(
 
389
                "Unknown bisect command: " + subcommand)
 
390
 
 
391
    def reset(self):
 
392
        """Reset the bisect state to no state."""
 
393
        self._check()
 
394
        BisectCurrent().reset()
 
395
        os.unlink(bisect_info_path)
 
396
 
 
397
    def start(self):
 
398
        """Reset the bisect state, then prepare for a new bisection."""
 
399
        if os.path.exists(bisect_info_path):
 
400
            BisectCurrent().reset()
 
401
            os.unlink(bisect_info_path)
 
402
 
 
403
        bisect_log = BisectLog()
 
404
        bisect_log.set_current("start")
 
405
        bisect_log.save()
 
406
 
 
407
    def yes(self, revspec):
 
408
        """Mark that a given revision has the state we're looking for."""
 
409
        self._set_state(revspec, "yes")
 
410
 
 
411
    def no(self, revspec):
 
412
        """Mark that a given revision does not have the state we're looking for."""
 
413
        self._set_state(revspec, "no")
 
414
 
 
415
    def move(self, revspec):
 
416
        """Move to a different revision manually."""
 
417
        current = BisectCurrent()
 
418
        current.switch(revspec)
 
419
        current.show_rev_log(out=self.outf)
 
420
 
 
421
    def log(self, filename):
 
422
        """Write the current bisect log to a file."""
 
423
        self._check()
 
424
        bisect_log = BisectLog()
 
425
        bisect_log.change_file_name(filename)
 
426
        bisect_log.save()
 
427
 
 
428
    def replay(self, filename):
 
429
        """Apply the given log file to a clean state, so the state is
 
430
        exactly as it was when the log was saved."""
 
431
        if os.path.exists(bisect_info_path):
 
432
            BisectCurrent().reset()
 
433
            os.unlink(bisect_info_path)
 
434
        bisect_log = BisectLog(filename)
 
435
        bisect_log.change_file_name(bisect_info_path)
 
436
        bisect_log.save()
 
437
 
 
438
        bisect_log.bisect(self.outf)
 
439
 
 
440
    def run_bisect(self, script):
 
441
        import subprocess
 
442
        note("Starting bisect.")
 
443
        self.start()
 
444
        while True:
 
445
            try:
 
446
                process = subprocess.Popen(script, shell=True)
 
447
                process.wait()
 
448
                retcode = process.returncode
 
449
                if retcode == 0:
 
450
                    done = self._set_state(None, 'yes')
 
451
                elif retcode == 125:
 
452
                    break
 
453
                else:
 
454
                    done = self._set_state(None, 'no')
 
455
                if done:
 
456
                    break
 
457
            except RuntimeError:
 
458
                break