/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-06-10 16:40:42 UTC
  • mfrom: (6653.6.7 rename-controldir)
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170610164042-zrxqgy2htyduvke2
MergeĀ rename-controldirĀ branch.

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