/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: Jelmer Vernooij
  • Date: 2017-09-01 07:15:43 UTC
  • mfrom: (6770.3.2 py3_test_cleanup)
  • Revision ID: jelmer@jelmer.uk-20170901071543-1t83321xkog9qrxh
Merge lp:~gz/brz/py3_test_cleanup

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 + "\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
        revdict = self._branch.get_revision_id_to_revno_map()
 
61
        return revdict[self.get_current_revid()]
 
62
 
 
63
    def get_parent_revids(self):
 
64
        """Return the IDs of the current revision's predecessors."""
 
65
        repo = self._branch.repository
 
66
        with repo.lock_read():
 
67
            retval = repo.get_parent_map([self._revid]).get(self._revid, None)
 
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._branch.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._controldir.open_workingtree()
 
84
        if isinstance(revid, int):
 
85
            revid = self._branch.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._controldir.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 self._controldir.control_transport.has(BISECT_REV_PATH):
 
100
            self._controldir.control_transport.delete(BISECT_REV_PATH)
 
101
 
 
102
 
 
103
class BisectLog(object):
 
104
    """Bisect log file handler."""
 
105
 
 
106
    def __init__(self, controldir, filename=BISECT_INFO_PATH):
 
107
        self._items = []
 
108
        self._current = BisectCurrent(controldir)
 
109
        self._controldir = controldir
 
110
        self._branch = None
 
111
        self._high_revid = None
 
112
        self._low_revid = None
 
113
        self._middle_revid = None
 
114
        self._filename = filename
 
115
        self.load()
 
116
 
 
117
    def _open_for_read(self):
 
118
        """Open log file for reading."""
 
119
        if self._filename:
 
120
            return self._controldir.control_transport.get(self._filename)
 
121
        else:
 
122
            return sys.stdin
 
123
 
 
124
    def _load_tree(self):
 
125
        """Load bzr information."""
 
126
        if not self._branch:
 
127
            self._branch = self._controldir.open_branch()
 
128
 
 
129
    def _find_range_and_middle(self, branch_last_rev = None):
 
130
        """Find the current revision range, and the midpoint."""
 
131
        self._load_tree()
 
132
        self._middle_revid = None
 
133
 
 
134
        if not branch_last_rev:
 
135
            last_revid = self._branch.last_revision()
 
136
        else:
 
137
            last_revid = branch_last_rev
 
138
 
 
139
        repo = self._branch.repository
 
140
        with repo.lock_read():
 
141
            graph = repo.get_graph()
 
142
            rev_sequence = graph.iter_lefthand_ancestry(last_revid,
 
143
                (_mod_revision.NULL_REVISION,))
 
144
            high_revid = None
 
145
            low_revid = None
 
146
            between_revs = []
 
147
            for revision in rev_sequence:
 
148
                between_revs.insert(0, revision)
 
149
                matches = [x[1] for x in self._items
 
150
                           if x[0] == revision and x[1] in ('yes', 'no')]
 
151
                if not matches:
 
152
                    continue
 
153
                if len(matches) > 1:
 
154
                    raise RuntimeError("revision %s duplicated" % revision)
 
155
                if matches[0] == "yes":
 
156
                    high_revid = revision
 
157
                    between_revs = []
 
158
                elif matches[0] == "no":
 
159
                    low_revid = revision
 
160
                    del between_revs[0]
 
161
                    break
 
162
 
 
163
            if not high_revid:
 
164
                high_revid = last_revid
 
165
            if not low_revid:
 
166
                low_revid = self._branch.get_rev_id(1)
 
167
 
 
168
        # The spread must include the high revision, to bias
 
169
        # odd numbers of intervening revisions towards the high
 
170
        # side.
 
171
 
 
172
        spread = len(between_revs) + 1
 
173
        if spread < 2:
 
174
            middle_index = 0
 
175
        else:
 
176
            middle_index = (spread / 2) - 1
 
177
 
 
178
        if len(between_revs) > 0:
 
179
            self._middle_revid = between_revs[middle_index]
 
180
        else:
 
181
            self._middle_revid = high_revid
 
182
 
 
183
        self._high_revid = high_revid
 
184
        self._low_revid = low_revid
 
185
 
 
186
    def _switch_wc_to_revno(self, revno, outf):
 
187
        """Move the working tree to the given revno."""
 
188
        self._current.switch(revno)
 
189
        self._current.show_rev_log(out=outf)
 
190
 
 
191
    def _set_status(self, revid, status):
 
192
        """Set the bisect status for the given revid."""
 
193
        if not self.is_done():
 
194
            if status != "done" and revid in [x[0] for x in self._items
 
195
                                              if x[1] in ['yes', 'no']]:
 
196
                raise RuntimeError("attempting to add revid %s twice" % revid)
 
197
            self._items.append((revid, status))
 
198
 
 
199
    def change_file_name(self, filename):
 
200
        """Switch log files."""
 
201
        self._filename = filename
 
202
 
 
203
    def load(self):
 
204
        """Load the bisection log."""
 
205
        self._items = []
 
206
        if self._controldir.control_transport.has(self._filename):
 
207
            revlog = self._open_for_read()
 
208
            for line in revlog:
 
209
                (revid, status) = line.split()
 
210
                self._items.append((revid, status))
 
211
 
 
212
    def save(self):
 
213
        """Save the bisection log."""
 
214
        contents = ''.join(
 
215
            ("%s %s\n" % (revid, status))
 
216
            for (revid, status) in self._items)
 
217
        if self._filename:
 
218
            self._controldir.control_transport.put_bytes(
 
219
                self._filename, contents)
 
220
        else:
 
221
            sys.stdout.write(contents)
 
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_tree()
 
230
        revid = revspec[0].in_history(self._branch).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._branch.repository
 
242
        with repo.lock_read():
 
243
            retval = repo.get_parent_map([revid]).get(revid, None)
 
244
        return retval
 
245
 
 
246
    def bisect(self, outf):
 
247
        """Using the current revision's status, do a bisection."""
 
248
        self._find_range_and_middle()
 
249
        # If we've found the "final" revision, check for a
 
250
        # merge point.
 
251
        while ((self._middle_revid == self._high_revid
 
252
                or self._middle_revid == self._low_revid)
 
253
                and self.is_merge_point(self._middle_revid)):
 
254
            for parent in self.get_parent_revids(self._middle_revid):
 
255
                if parent == self._low_revid:
 
256
                    continue
 
257
                else:
 
258
                    self._find_range_and_middle(parent)
 
259
                    break
 
260
        self._switch_wc_to_revno(self._middle_revid, outf)
 
261
        if self._middle_revid == self._high_revid or \
 
262
           self._middle_revid == self._low_revid:
 
263
            self.set_current("done")
 
264
 
 
265
 
 
266
class cmd_bisect(Command):
 
267
    """Find an interesting commit using a binary search.
 
268
 
 
269
    Bisecting, in a nutshell, is a way to find the commit at which
 
270
    some testable change was made, such as the introduction of a bug
 
271
    or feature.  By identifying a version which did not have the
 
272
    interesting change and a later version which did, a developer
 
273
    can test for the presence of the change at various points in
 
274
    the history, eventually ending up at the precise commit when
 
275
    the change was first introduced.
 
276
 
 
277
    This command uses subcommands to implement the search, each
 
278
    of which changes the state of the bisection.  The
 
279
    subcommands are:
 
280
 
 
281
    brz bisect start
 
282
        Start a bisect, possibly clearing out a previous bisect.
 
283
 
 
284
    brz bisect yes [-r rev]
 
285
        The specified revision (or the current revision, if not given)
 
286
        has the characteristic we're looking for,
 
287
 
 
288
    brz bisect no [-r rev]
 
289
        The specified revision (or the current revision, if not given)
 
290
        does not have the characteristic we're looking for,
 
291
 
 
292
    brz bisect move -r rev
 
293
        Switch to a different revision manually.  Use if the bisect
 
294
        algorithm chooses a revision that is not suitable.  Try to
 
295
        move as little as possible.
 
296
 
 
297
    brz bisect reset
 
298
        Clear out a bisection in progress.
 
299
 
 
300
    brz bisect log [-o file]
 
301
        Output a log of the current bisection to standard output, or
 
302
        to the specified file.
 
303
 
 
304
    brz bisect replay <logfile>
 
305
        Replay a previously-saved bisect log, forgetting any bisection
 
306
        that might be in progress.
 
307
 
 
308
    brz bisect run <script>
 
309
        Bisect automatically using <script> to determine 'yes' or 'no'.
 
310
        <script> should exit with:
 
311
           0 for yes
 
312
           125 for unknown (like build failed so we could not test)
 
313
           anything else for no
 
314
    """
 
315
 
 
316
    takes_args = ['subcommand', 'args*']
 
317
    takes_options = [Option('output', short_name='o',
 
318
                            help='Write log to this file.', type=text_type),
 
319
                     'revision', 'directory']
 
320
 
 
321
    def _check(self, controldir):
 
322
        """Check preconditions for most operations to work."""
 
323
        if not controldir.control_transport.has(BISECT_INFO_PATH):
 
324
            raise BzrCommandError("No bisection in progress.")
 
325
 
 
326
    def _set_state(self, controldir, revspec, state):
 
327
        """Set the state of the given revspec and bisecting.
 
328
 
 
329
        Returns boolean indicating if bisection is done."""
 
330
        bisect_log = BisectLog(controldir)
 
331
        if bisect_log.is_done():
 
332
            note("No further bisection is possible.\n")
 
333
            bisect_log._current.show_rev_log(self.outf)
 
334
            return True
 
335
 
 
336
        if revspec:
 
337
            bisect_log.set_status_from_revspec(revspec, state)
 
338
        else:
 
339
            bisect_log.set_current(state)
 
340
        bisect_log.bisect(self.outf)
 
341
        bisect_log.save()
 
342
        return False
 
343
 
 
344
    def run(self, subcommand, args_list, directory='.', revision=None, 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 that a given revision does not have the state we're looking for."""
 
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(out=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