/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: John Ferlito
  • Date: 2009-09-02 04:31:45 UTC
  • mto: (4665.7.1 serve-init)
  • mto: This revision was merged to the branch mainline in revision 4913.
  • Revision ID: johnf@inodes.org-20090902043145-gxdsfw03ilcwbyn5
Add a debian init script for bzr --serve

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