/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.62.1 by Jelmer Vernooij
Lazy load plugin.
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
6625.4.4 by Jelmer Vernooij
Drop meta file.
19
from __future__ import absolute_import
20
0.62.1 by Jelmer Vernooij
Lazy load plugin.
21
import sys
22
import os
6625.4.1 by Jelmer Vernooij
Merge bisect.
23
from ...bzrdir import BzrDir
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
0.62.1 by Jelmer Vernooij
Lazy load plugin.
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
6625.4.1 by Jelmer Vernooij
Merge bisect.
39
        self._bzrdir = BzrDir.open_containing(".")[0]
0.62.1 by Jelmer Vernooij
Lazy load plugin.
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:
6625.4.1 by Jelmer Vernooij
Merge bisect.
135
            self._bzrdir = BzrDir.open_containing('.')[0]
0.62.1 by Jelmer Vernooij
Lazy load plugin.
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:
0.63.1 by Thomi Richards
Use graph.iter_lefthand_ancestry instead of repo.iter_reverse_revision_history.
151
            graph = repo.get_graph()
152
            rev_sequence = graph.iter_lefthand_ancestry(last_revid,
153
                (_mod_revision.NULL_REVISION,))
0.62.1 by Jelmer Vernooij
Lazy load plugin.
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():
0.63.1 by Thomi Richards
Use graph.iter_lefthand_ancestry instead of repo.iter_reverse_revision_history.
206
            if status != "done" and revid in [x[0] for x in self._items
0.62.1 by Jelmer Vernooij
Lazy load plugin.
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
    bzr bisect start
292
        Start a bisect, possibly clearing out a previous bisect.
293
294
    bzr 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
    bzr 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
    bzr 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
    bzr bisect reset
308
        Clear out a bisection in progress.
309
310
    bzr bisect log [-o file]
311
        Output a log of the current bisection to standard output, or
312
        to the specified file.
313
314
    bzr bisect replay <logfile>
315
        Replay a previously-saved bisect log, forgetting any bisection
316
        that might be in progress.
317
318
    bzr 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