~bzr/bzr-bisect/trunk

« back to all changes in this revision

Viewing changes to __init__.py

  • Committer: Jelmer Vernooij
  • Date: 2011-11-25 13:21:00 UTC
  • mfrom: (82.1.1 lazy)
  • Revision ID: jelmer@samba.org-20111125132100-6rr5ftv20h1w3tds
Merge support for lazy loading.

Show diffs side-by-side

added added

removed removed

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