/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: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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