/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 breezy/bisect.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-04-24 10:31:28 UTC
  • mfrom: (1684.1.2 bzr.mbp.integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060424103128-a637f56a7c529bad
(mbp) tutorial improvements

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""bisect command implementations."""
18
 
 
19
 
from __future__ import absolute_import
20
 
 
21
 
import sys
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 .sixish import (
28
 
    text_type,
29
 
    )
30
 
from .trace import note
31
 
 
32
 
BISECT_INFO_PATH = "bisect"
33
 
BISECT_REV_PATH = "bisect_revid"
34
 
 
35
 
 
36
 
class BisectCurrent(object):
37
 
    """Bisect class for managing the current revision."""
38
 
 
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(
45
 
                filename).strip()
46
 
        else:
47
 
            self._revid = self._branch.last_revision()
48
 
 
49
 
    def _save(self):
50
 
        """Save the current revision."""
51
 
        self._controldir.control_transport.put_bytes(
52
 
            self._filename, self._revid + b"\n")
53
 
 
54
 
    def get_current_revid(self):
55
 
        """Return the current revision id."""
56
 
        return self._revid
57
 
 
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)
61
 
 
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)
67
 
        return retval
68
 
 
69
 
    def is_merge_point(self):
70
 
        """Is the current revision a merge point?"""
71
 
        return len(self.get_parent_revids()) > 1
72
 
 
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,
78
 
                                                   rev.message))
79
 
 
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),
88
 
                       False)
89
 
        self._revid = revid
90
 
        self._save()
91
 
 
92
 
    def reset(self):
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)
100
 
 
101
 
 
102
 
class BisectLog(object):
103
 
    """Bisect log file handler."""
104
 
 
105
 
    def __init__(self, controldir, filename=BISECT_INFO_PATH):
106
 
        self._items = []
107
 
        self._current = BisectCurrent(controldir)
108
 
        self._controldir = controldir
109
 
        self._branch = 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 self._controldir.control_transport.get(self._filename)
120
 
        else:
121
 
            return sys.stdin
122
 
 
123
 
    def _load_tree(self):
124
 
        """Load bzr information."""
125
 
        if not self._branch:
126
 
            self._branch = self._controldir.open_branch()
127
 
 
128
 
    def _find_range_and_middle(self, branch_last_rev=None):
129
 
        """Find the current revision range, and the midpoint."""
130
 
        self._load_tree()
131
 
        self._middle_revid = None
132
 
 
133
 
        if not branch_last_rev:
134
 
            last_revid = self._branch.last_revision()
135
 
        else:
136
 
            last_revid = branch_last_rev
137
 
 
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,))
143
 
            high_revid = None
144
 
            low_revid = None
145
 
            between_revs = []
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')]
150
 
                if not matches:
151
 
                    continue
152
 
                if len(matches) > 1:
153
 
                    raise RuntimeError("revision %s duplicated" % revision)
154
 
                if matches[0] == "yes":
155
 
                    high_revid = revision
156
 
                    between_revs = []
157
 
                elif matches[0] == "no":
158
 
                    low_revid = revision
159
 
                    del between_revs[0]
160
 
                    break
161
 
 
162
 
            if not high_revid:
163
 
                high_revid = last_revid
164
 
            if not low_revid:
165
 
                low_revid = self._branch.get_rev_id(1)
166
 
 
167
 
        # The spread must include the high revision, to bias
168
 
        # odd numbers of intervening revisions towards the high
169
 
        # side.
170
 
 
171
 
        spread = len(between_revs) + 1
172
 
        if spread < 2:
173
 
            middle_index = 0
174
 
        else:
175
 
            middle_index = (spread // 2) - 1
176
 
 
177
 
        if len(between_revs) > 0:
178
 
            self._middle_revid = between_revs[middle_index]
179
 
        else:
180
 
            self._middle_revid = high_revid
181
 
 
182
 
        self._high_revid = high_revid
183
 
        self._low_revid = low_revid
184
 
 
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)
189
 
 
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))
197
 
 
198
 
    def change_file_name(self, filename):
199
 
        """Switch log files."""
200
 
        self._filename = filename
201
 
 
202
 
    def load(self):
203
 
        """Load the bisection log."""
204
 
        self._items = []
205
 
        if self._controldir.control_transport.has(self._filename):
206
 
            revlog = self._open_for_read()
207
 
            for line in revlog:
208
 
                (revid, status) = line.split()
209
 
                self._items.append((revid, status.decode('ascii')))
210
 
 
211
 
    def save(self):
212
 
        """Save the bisection log."""
213
 
        contents = b''.join(
214
 
            (b"%s %s\n" % (revid, status.encode('ascii')))
215
 
            for (revid, status) in self._items)
216
 
        if self._filename:
217
 
            self._controldir.control_transport.put_bytes(
218
 
                self._filename, contents)
219
 
        else:
220
 
            sys.stdout.write(contents)
221
 
 
222
 
    def is_done(self):
223
 
        """Report whether we've found the right revision."""
224
 
        return len(self._items) > 0 and self._items[-1][1] == "done"
225
 
 
226
 
    def set_status_from_revspec(self, revspec, status):
227
 
        """Set the bisection status for the revision in revspec."""
228
 
        self._load_tree()
229
 
        revid = revspec[0].in_history(self._branch).rev_id
230
 
        self._set_status(revid, status)
231
 
 
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)
235
 
 
236
 
    def is_merge_point(self, revid):
237
 
        return len(self.get_parent_revids(revid)) > 1
238
 
 
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)
243
 
        return retval
244
 
 
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
249
 
        # merge point.
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:
255
 
                    continue
256
 
                else:
257
 
                    self._find_range_and_middle(parent)
258
 
                    break
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")
263
 
 
264
 
 
265
 
class cmd_bisect(Command):
266
 
    """Find an interesting commit using a binary search.
267
 
 
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.
275
 
 
276
 
    This command uses subcommands to implement the search, each
277
 
    of which changes the state of the bisection.  The
278
 
    subcommands are:
279
 
 
280
 
    brz bisect start
281
 
        Start a bisect, possibly clearing out a previous bisect.
282
 
 
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,
286
 
 
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,
290
 
 
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.
295
 
 
296
 
    brz bisect reset
297
 
        Clear out a bisection in progress.
298
 
 
299
 
    brz bisect log [-o file]
300
 
        Output a log of the current bisection to standard output, or
301
 
        to the specified file.
302
 
 
303
 
    brz bisect replay <logfile>
304
 
        Replay a previously-saved bisect log, forgetting any bisection
305
 
        that might be in progress.
306
 
 
307
 
    brz bisect run <script>
308
 
        Bisect automatically using <script> to determine 'yes' or 'no'.
309
 
        <script> should exit with:
310
 
           0 for yes
311
 
           125 for unknown (like build failed so we could not test)
312
 
           anything else for no
313
 
    """
314
 
 
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']
319
 
 
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.")
324
 
 
325
 
    def _set_state(self, controldir, revspec, state):
326
 
        """Set the state of the given revspec and bisecting.
327
 
 
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)
333
 
            return True
334
 
 
335
 
        if revspec:
336
 
            bisect_log.set_status_from_revspec(revspec, state)
337
 
        else:
338
 
            bisect_log.set_current(state)
339
 
        bisect_log.bisect(self.outf)
340
 
        bisect_log.save()
341
 
        return False
342
 
 
343
 
    def run(self, subcommand, args_list, directory='.', revision=None,
344
 
            output=None):
345
 
        """Handle the bisect command."""
346
 
 
347
 
        log_fn = None
348
 
        if subcommand in ('yes', 'no', 'move') and revision:
349
 
            pass
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)
360
 
 
361
 
        controldir, _ = ControlDir.open_containing(directory)
362
 
 
363
 
        # Dispatch.
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)
380
 
        else:
381
 
            raise BzrCommandError(
382
 
                "Unknown bisect command: " + subcommand)
383
 
 
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)
389
 
 
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)
395
 
 
396
 
        bisect_log = BisectLog(controldir)
397
 
        bisect_log.set_current("start")
398
 
        bisect_log.save()
399
 
 
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")
403
 
 
404
 
    def no(self, controldir, revspec):
405
 
        """Mark a given revision as wrong."""
406
 
        self._set_state(controldir, revspec, "no")
407
 
 
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)
413
 
 
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)
419
 
        bisect_log.save()
420
 
 
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)
429
 
        bisect_log.save()
430
 
 
431
 
        bisect_log.bisect(self.outf)
432
 
 
433
 
    def run_bisect(self, controldir, script):
434
 
        import subprocess
435
 
        note("Starting bisect.")
436
 
        self.start(controldir)
437
 
        while True:
438
 
            try:
439
 
                process = subprocess.Popen(script, shell=True)
440
 
                process.wait()
441
 
                retcode = process.returncode
442
 
                if retcode == 0:
443
 
                    done = self._set_state(controldir, None, 'yes')
444
 
                elif retcode == 125:
445
 
                    break
446
 
                else:
447
 
                    done = self._set_state(controldir, None, 'no')
448
 
                if done:
449
 
                    break
450
 
            except RuntimeError:
451
 
                break