Merge lp:~jelmer/bzr-bisect/lazy into lp:bzr-bisect
- lazy
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Merged |
---|---|
Merged at revision: | 83 |
Proposed branch: | lp:~jelmer/bzr-bisect/lazy |
Merge into: | lp:bzr-bisect |
Diff against target: |
1041 lines (+478/-461) 3 files modified
__init__.py (+3/-440) cmds.py (+455/-0) tests.py (+20/-21) |
To merge this branch: | bzr merge lp:~jelmer/bzr-bisect/lazy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pool (community) | Approve | ||
Review via email: mp+83319@code.launchpad.net |
Commit message
Description of the change
Lazily load bisect commands.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '__init__.py' |
2 | --- __init__.py 2010-11-05 12:25:01 +0000 |
3 | +++ __init__.py 2011-11-24 16:12:23 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -# Copyright (C) 2006-2010 Canonical Ltd |
6 | +# Copyright (C) 2006-2011 Canonical Ltd |
7 | # |
8 | # This program is free software; you can redistribute it and/or modify |
9 | # it under the terms of the GNU General Public License as published by |
10 | @@ -16,448 +16,11 @@ |
11 | |
12 | """Support for git-style bisection.""" |
13 | |
14 | -import sys |
15 | -import os |
16 | -import bzrlib.bzrdir |
17 | -from bzrlib.commands import Command, register_command |
18 | -from bzrlib.errors import BzrCommandError |
19 | -from bzrlib.option import Option |
20 | -from bzrlib.trace import note |
21 | +from bzrlib.commands import plugin_cmds |
22 | |
23 | from meta import * |
24 | |
25 | -bisect_info_path = ".bzr/bisect" |
26 | -bisect_rev_path = ".bzr/bisect_revid" |
27 | - |
28 | - |
29 | -class BisectCurrent(object): |
30 | - """Bisect class for managing the current revision.""" |
31 | - |
32 | - def __init__(self, filename = bisect_rev_path): |
33 | - self._filename = filename |
34 | - self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing(".")[0] |
35 | - self._bzrbranch = self._bzrdir.open_branch() |
36 | - if os.path.exists(filename): |
37 | - revid_file = open(filename) |
38 | - self._revid = revid_file.read().strip() |
39 | - revid_file.close() |
40 | - else: |
41 | - self._revid = self._bzrbranch.last_revision() |
42 | - |
43 | - def _save(self): |
44 | - """Save the current revision.""" |
45 | - |
46 | - revid_file = open(self._filename, "w") |
47 | - revid_file.write(self._revid + "\n") |
48 | - revid_file.close() |
49 | - |
50 | - def get_current_revid(self): |
51 | - """Return the current revision id.""" |
52 | - return self._revid |
53 | - |
54 | - def get_current_revno(self): |
55 | - """Return the current revision number as a tuple.""" |
56 | - revdict = self._bzrbranch.get_revision_id_to_revno_map() |
57 | - return revdict[self.get_current_revid()] |
58 | - |
59 | - def get_parent_revids(self): |
60 | - """Return the IDs of the current revision's predecessors.""" |
61 | - repo = self._bzrbranch.repository |
62 | - repo.lock_read() |
63 | - retval = repo.get_parent_map([self._revid]).get(self._revid, None) |
64 | - repo.unlock() |
65 | - return retval |
66 | - |
67 | - def is_merge_point(self): |
68 | - """Is the current revision a merge point?""" |
69 | - return len(self.get_parent_revids()) > 1 |
70 | - |
71 | - def show_rev_log(self, out = sys.stdout): |
72 | - """Write the current revision's log entry to a file.""" |
73 | - rev = self._bzrbranch.repository.get_revision(self._revid) |
74 | - revno = ".".join([str(x) for x in self.get_current_revno()]) |
75 | - out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id, |
76 | - rev.message)) |
77 | - |
78 | - def switch(self, revid): |
79 | - """Switch the current revision to the given revid.""" |
80 | - working = self._bzrdir.open_workingtree() |
81 | - if isinstance(revid, int): |
82 | - revid = self._bzrbranch.get_rev_id(revid) |
83 | - elif isinstance(revid, list): |
84 | - revid = revid[0].in_history(working.branch).rev_id |
85 | - working.revert(None, working.branch.repository.revision_tree(revid), |
86 | - False) |
87 | - self._revid = revid |
88 | - self._save() |
89 | - |
90 | - def reset(self): |
91 | - """Revert bisection, setting the working tree to normal.""" |
92 | - working = self._bzrdir.open_workingtree() |
93 | - last_rev = working.branch.last_revision() |
94 | - rev_tree = working.branch.repository.revision_tree(last_rev) |
95 | - working.revert(None, rev_tree, False) |
96 | - if os.path.exists(bisect_rev_path): |
97 | - os.unlink(bisect_rev_path) |
98 | - |
99 | - |
100 | -class BisectLog(object): |
101 | - """Bisect log file handler.""" |
102 | - |
103 | - def __init__(self, filename = bisect_info_path): |
104 | - self._items = [] |
105 | - self._current = BisectCurrent() |
106 | - self._bzrdir = 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 open(self._filename) |
117 | - else: |
118 | - return sys.stdin |
119 | - |
120 | - def _open_for_write(self): |
121 | - """Open log file for writing.""" |
122 | - if self._filename: |
123 | - return open(self._filename, "w") |
124 | - else: |
125 | - return sys.stdout |
126 | - |
127 | - def _load_bzr_tree(self): |
128 | - """Load bzr information.""" |
129 | - if not self._bzrdir: |
130 | - self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing('.')[0] |
131 | - self._bzrbranch = self._bzrdir.open_branch() |
132 | - |
133 | - def _find_range_and_middle(self, branch_last_rev = None): |
134 | - """Find the current revision range, and the midpoint.""" |
135 | - self._load_bzr_tree() |
136 | - self._middle_revid = None |
137 | - |
138 | - if not branch_last_rev: |
139 | - last_revid = self._bzrbranch.last_revision() |
140 | - else: |
141 | - last_revid = branch_last_rev |
142 | - |
143 | - repo = self._bzrbranch.repository |
144 | - repo.lock_read() |
145 | - try: |
146 | - rev_sequence = repo.iter_reverse_revision_history(last_revid) |
147 | - high_revid = None |
148 | - low_revid = None |
149 | - between_revs = [] |
150 | - for revision in rev_sequence: |
151 | - between_revs.insert(0, revision) |
152 | - matches = [x[1] for x in self._items |
153 | - if x[0] == revision and x[1] in ('yes', 'no')] |
154 | - if not matches: |
155 | - continue |
156 | - if len(matches) > 1: |
157 | - raise RuntimeError("revision %s duplicated" % revision) |
158 | - if matches[0] == "yes": |
159 | - high_revid = revision |
160 | - between_revs = [] |
161 | - elif matches[0] == "no": |
162 | - low_revid = revision |
163 | - del between_revs[0] |
164 | - break |
165 | - |
166 | - if not high_revid: |
167 | - high_revid = last_revid |
168 | - if not low_revid: |
169 | - low_revid = self._bzrbranch.get_rev_id(1) |
170 | - finally: |
171 | - repo.unlock() |
172 | - |
173 | - # The spread must include the high revision, to bias |
174 | - # odd numbers of intervening revisions towards the high |
175 | - # side. |
176 | - |
177 | - spread = len(between_revs) + 1 |
178 | - if spread < 2: |
179 | - middle_index = 0 |
180 | - else: |
181 | - middle_index = (spread / 2) - 1 |
182 | - |
183 | - if len(between_revs) > 0: |
184 | - self._middle_revid = between_revs[middle_index] |
185 | - else: |
186 | - self._middle_revid = high_revid |
187 | - |
188 | - self._high_revid = high_revid |
189 | - self._low_revid = low_revid |
190 | - |
191 | - def _switch_wc_to_revno(self, revno, outf): |
192 | - """Move the working tree to the given revno.""" |
193 | - self._current.switch(revno) |
194 | - self._current.show_rev_log(out=outf) |
195 | - |
196 | - def _set_status(self, revid, status): |
197 | - """Set the bisect status for the given revid.""" |
198 | - if not self.is_done(): |
199 | - if status != "done" and revid in [x[0] for x in self._items |
200 | - if x[1] in ['yes', 'no']]: |
201 | - raise RuntimeError("attempting to add revid %s twice" % revid) |
202 | - self._items.append((revid, status)) |
203 | - |
204 | - def change_file_name(self, filename): |
205 | - """Switch log files.""" |
206 | - self._filename = filename |
207 | - |
208 | - def load(self): |
209 | - """Load the bisection log.""" |
210 | - self._items = [] |
211 | - if os.path.exists(self._filename): |
212 | - revlog = self._open_for_read() |
213 | - for line in revlog: |
214 | - (revid, status) = line.split() |
215 | - self._items.append((revid, status)) |
216 | - |
217 | - def save(self): |
218 | - """Save the bisection log.""" |
219 | - revlog = self._open_for_write() |
220 | - for (revid, status) in self._items: |
221 | - revlog.write("%s %s\n" % (revid, status)) |
222 | - |
223 | - def is_done(self): |
224 | - """Report whether we've found the right revision.""" |
225 | - return len(self._items) > 0 and self._items[-1][1] == "done" |
226 | - |
227 | - def set_status_from_revspec(self, revspec, status): |
228 | - """Set the bisection status for the revision in revspec.""" |
229 | - self._load_bzr_tree() |
230 | - revid = revspec[0].in_history(self._bzrbranch).rev_id |
231 | - self._set_status(revid, status) |
232 | - |
233 | - def set_current(self, status): |
234 | - """Set the current revision to the given bisection status.""" |
235 | - self._set_status(self._current.get_current_revid(), status) |
236 | - |
237 | - def is_merge_point(self, revid): |
238 | - return len(self.get_parent_revids(revid)) > 1 |
239 | - |
240 | - def get_parent_revids(self, revid): |
241 | - repo = self._bzrbranch.repository |
242 | - repo.lock_read() |
243 | - try: |
244 | - retval = repo.get_parent_map([revid]).get(revid, None) |
245 | - finally: |
246 | - repo.unlock() |
247 | - return retval |
248 | - |
249 | - def bisect(self, outf): |
250 | - """Using the current revision's status, do a bisection.""" |
251 | - self._find_range_and_middle() |
252 | - # If we've found the "final" revision, check for a |
253 | - # merge point. |
254 | - while ((self._middle_revid == self._high_revid |
255 | - or self._middle_revid == self._low_revid) |
256 | - and self.is_merge_point(self._middle_revid)): |
257 | - for parent in self.get_parent_revids(self._middle_revid): |
258 | - if parent == self._low_revid: |
259 | - continue |
260 | - else: |
261 | - self._find_range_and_middle(parent) |
262 | - break |
263 | - self._switch_wc_to_revno(self._middle_revid, outf) |
264 | - if self._middle_revid == self._high_revid or \ |
265 | - self._middle_revid == self._low_revid: |
266 | - self.set_current("done") |
267 | - |
268 | - |
269 | -class cmd_bisect(Command): |
270 | - """Find an interesting commit using a binary search. |
271 | - |
272 | - Bisecting, in a nutshell, is a way to find the commit at which |
273 | - some testable change was made, such as the introduction of a bug |
274 | - or feature. By identifying a version which did not have the |
275 | - interesting change and a later version which did, a developer |
276 | - can test for the presence of the change at various points in |
277 | - the history, eventually ending up at the precise commit when |
278 | - the change was first introduced. |
279 | - |
280 | - This command uses subcommands to implement the search, each |
281 | - of which changes the state of the bisection. The |
282 | - subcommands are: |
283 | - |
284 | - bzr bisect start |
285 | - Start a bisect, possibly clearing out a previous bisect. |
286 | - |
287 | - bzr bisect yes [-r rev] |
288 | - The specified revision (or the current revision, if not given) |
289 | - has the characteristic we're looking for, |
290 | - |
291 | - bzr bisect no [-r rev] |
292 | - The specified revision (or the current revision, if not given) |
293 | - does not have the characteristic we're looking for, |
294 | - |
295 | - bzr bisect move -r rev |
296 | - Switch to a different revision manually. Use if the bisect |
297 | - algorithm chooses a revision that is not suitable. Try to |
298 | - move as little as possible. |
299 | - |
300 | - bzr bisect reset |
301 | - Clear out a bisection in progress. |
302 | - |
303 | - bzr bisect log [-o file] |
304 | - Output a log of the current bisection to standard output, or |
305 | - to the specified file. |
306 | - |
307 | - bzr bisect replay <logfile> |
308 | - Replay a previously-saved bisect log, forgetting any bisection |
309 | - that might be in progress. |
310 | - |
311 | - bzr bisect run <script> |
312 | - Bisect automatically using <script> to determine 'yes' or 'no'. |
313 | - <script> should exit with: |
314 | - 0 for yes |
315 | - 125 for unknown (like build failed so we could not test) |
316 | - anything else for no |
317 | - """ |
318 | - |
319 | - takes_args = ['subcommand', 'args*'] |
320 | - takes_options = [Option('output', short_name='o', |
321 | - help='Write log to this file.', type=unicode), |
322 | - 'revision'] |
323 | - |
324 | - def _check(self): |
325 | - """Check preconditions for most operations to work.""" |
326 | - if not os.path.exists(bisect_info_path): |
327 | - raise BzrCommandError("No bisection in progress.") |
328 | - |
329 | - def _set_state(self, revspec, state): |
330 | - """Set the state of the given revspec and bisecting. |
331 | - |
332 | - Returns boolean indicating if bisection is done.""" |
333 | - bisect_log = BisectLog() |
334 | - if bisect_log.is_done(): |
335 | - note("No further bisection is possible.\n") |
336 | - bisect_log._current.show_rev_log(self.outf) |
337 | - return True |
338 | - |
339 | - if revspec: |
340 | - bisect_log.set_status_from_revspec(revspec, state) |
341 | - else: |
342 | - bisect_log.set_current(state) |
343 | - bisect_log.bisect(self.outf) |
344 | - bisect_log.save() |
345 | - return False |
346 | - |
347 | - def run(self, subcommand, args_list, revision=None, output=None): |
348 | - """Handle the bisect command.""" |
349 | - |
350 | - log_fn = None |
351 | - if subcommand in ('yes', 'no', 'move') and revision: |
352 | - pass |
353 | - elif subcommand in ('replay', ) and args_list and len(args_list) == 1: |
354 | - log_fn = args_list[0] |
355 | - elif subcommand in ('move', ) and not revision: |
356 | - raise BzrCommandError( |
357 | - "The 'bisect move' command requires a revision.") |
358 | - elif subcommand in ('run', ): |
359 | - run_script = args_list[0] |
360 | - elif args_list or revision: |
361 | - raise BzrCommandError( |
362 | - "Improper arguments to bisect " + subcommand) |
363 | - |
364 | - # Dispatch. |
365 | - |
366 | - if subcommand == "start": |
367 | - self.start() |
368 | - elif subcommand == "yes": |
369 | - self.yes(revision) |
370 | - elif subcommand == "no": |
371 | - self.no(revision) |
372 | - elif subcommand == "move": |
373 | - self.move(revision) |
374 | - elif subcommand == "reset": |
375 | - self.reset() |
376 | - elif subcommand == "log": |
377 | - self.log(output) |
378 | - elif subcommand == "replay": |
379 | - self.replay(log_fn) |
380 | - elif subcommand == "run": |
381 | - self.run_bisect(run_script) |
382 | - else: |
383 | - raise BzrCommandError( |
384 | - "Unknown bisect command: " + subcommand) |
385 | - |
386 | - def reset(self): |
387 | - """Reset the bisect state to no state.""" |
388 | - self._check() |
389 | - BisectCurrent().reset() |
390 | - os.unlink(bisect_info_path) |
391 | - |
392 | - def start(self): |
393 | - """Reset the bisect state, then prepare for a new bisection.""" |
394 | - if os.path.exists(bisect_info_path): |
395 | - BisectCurrent().reset() |
396 | - os.unlink(bisect_info_path) |
397 | - |
398 | - bisect_log = BisectLog() |
399 | - bisect_log.set_current("start") |
400 | - bisect_log.save() |
401 | - |
402 | - def yes(self, revspec): |
403 | - """Mark that a given revision has the state we're looking for.""" |
404 | - self._set_state(revspec, "yes") |
405 | - |
406 | - def no(self, revspec): |
407 | - """Mark that a given revision does not have the state we're looking for.""" |
408 | - self._set_state(revspec, "no") |
409 | - |
410 | - def move(self, revspec): |
411 | - """Move to a different revision manually.""" |
412 | - current = BisectCurrent() |
413 | - current.switch(revspec) |
414 | - current.show_rev_log(out=self.outf) |
415 | - |
416 | - def log(self, filename): |
417 | - """Write the current bisect log to a file.""" |
418 | - self._check() |
419 | - bisect_log = BisectLog() |
420 | - bisect_log.change_file_name(filename) |
421 | - bisect_log.save() |
422 | - |
423 | - def replay(self, filename): |
424 | - """Apply the given log file to a clean state, so the state is |
425 | - exactly as it was when the log was saved.""" |
426 | - if os.path.exists(bisect_info_path): |
427 | - BisectCurrent().reset() |
428 | - os.unlink(bisect_info_path) |
429 | - bisect_log = BisectLog(filename) |
430 | - bisect_log.change_file_name(bisect_info_path) |
431 | - bisect_log.save() |
432 | - |
433 | - bisect_log.bisect(self.outf) |
434 | - |
435 | - def run_bisect(self, script): |
436 | - import subprocess |
437 | - note("Starting bisect.") |
438 | - self.start() |
439 | - while True: |
440 | - try: |
441 | - process = subprocess.Popen(script, shell=True) |
442 | - process.wait() |
443 | - retcode = process.returncode |
444 | - if retcode == 0: |
445 | - done = self._set_state(None, 'yes') |
446 | - elif retcode == 125: |
447 | - break |
448 | - else: |
449 | - done = self._set_state(None, 'no') |
450 | - if done: |
451 | - break |
452 | - except RuntimeError: |
453 | - break |
454 | - |
455 | -register_command(cmd_bisect) |
456 | - |
457 | +plugin_cmds.register_lazy('cmd_bisect', [], 'bzrlib.plugins.bisect.cmds') |
458 | |
459 | def load_tests(basic_tests, module, loader): |
460 | testmod_names = [ |
461 | |
462 | === added file 'cmds.py' |
463 | --- cmds.py 1970-01-01 00:00:00 +0000 |
464 | +++ cmds.py 2011-11-24 16:12:23 +0000 |
465 | @@ -0,0 +1,455 @@ |
466 | +# Copyright (C) 2006-2011 Canonical Ltd |
467 | +# |
468 | +# This program is free software; you can redistribute it and/or modify |
469 | +# it under the terms of the GNU General Public License as published by |
470 | +# the Free Software Foundation; either version 2 of the License, or |
471 | +# (at your option) any later version. |
472 | +# |
473 | +# This program is distributed in the hope that it will be useful, |
474 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
475 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
476 | +# GNU General Public License for more details. |
477 | +# |
478 | +# You should have received a copy of the GNU General Public License |
479 | +# along with this program; if not, write to the Free Software |
480 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
481 | + |
482 | +"""bisect command implementations.""" |
483 | + |
484 | +import sys |
485 | +import os |
486 | +import bzrlib.bzrdir |
487 | +from bzrlib.commands import Command |
488 | +from bzrlib.errors import BzrCommandError |
489 | +from bzrlib.option import Option |
490 | +from bzrlib.trace import note |
491 | + |
492 | +bisect_info_path = ".bzr/bisect" |
493 | +bisect_rev_path = ".bzr/bisect_revid" |
494 | + |
495 | + |
496 | +class BisectCurrent(object): |
497 | + """Bisect class for managing the current revision.""" |
498 | + |
499 | + def __init__(self, filename = bisect_rev_path): |
500 | + self._filename = filename |
501 | + self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing(".")[0] |
502 | + self._bzrbranch = self._bzrdir.open_branch() |
503 | + if os.path.exists(filename): |
504 | + revid_file = open(filename) |
505 | + self._revid = revid_file.read().strip() |
506 | + revid_file.close() |
507 | + else: |
508 | + self._revid = self._bzrbranch.last_revision() |
509 | + |
510 | + def _save(self): |
511 | + """Save the current revision.""" |
512 | + |
513 | + revid_file = open(self._filename, "w") |
514 | + revid_file.write(self._revid + "\n") |
515 | + revid_file.close() |
516 | + |
517 | + def get_current_revid(self): |
518 | + """Return the current revision id.""" |
519 | + return self._revid |
520 | + |
521 | + def get_current_revno(self): |
522 | + """Return the current revision number as a tuple.""" |
523 | + revdict = self._bzrbranch.get_revision_id_to_revno_map() |
524 | + return revdict[self.get_current_revid()] |
525 | + |
526 | + def get_parent_revids(self): |
527 | + """Return the IDs of the current revision's predecessors.""" |
528 | + repo = self._bzrbranch.repository |
529 | + repo.lock_read() |
530 | + retval = repo.get_parent_map([self._revid]).get(self._revid, None) |
531 | + repo.unlock() |
532 | + return retval |
533 | + |
534 | + def is_merge_point(self): |
535 | + """Is the current revision a merge point?""" |
536 | + return len(self.get_parent_revids()) > 1 |
537 | + |
538 | + def show_rev_log(self, out = sys.stdout): |
539 | + """Write the current revision's log entry to a file.""" |
540 | + rev = self._bzrbranch.repository.get_revision(self._revid) |
541 | + revno = ".".join([str(x) for x in self.get_current_revno()]) |
542 | + out.write("On revision %s (%s):\n%s\n" % (revno, rev.revision_id, |
543 | + rev.message)) |
544 | + |
545 | + def switch(self, revid): |
546 | + """Switch the current revision to the given revid.""" |
547 | + working = self._bzrdir.open_workingtree() |
548 | + if isinstance(revid, int): |
549 | + revid = self._bzrbranch.get_rev_id(revid) |
550 | + elif isinstance(revid, list): |
551 | + revid = revid[0].in_history(working.branch).rev_id |
552 | + working.revert(None, working.branch.repository.revision_tree(revid), |
553 | + False) |
554 | + self._revid = revid |
555 | + self._save() |
556 | + |
557 | + def reset(self): |
558 | + """Revert bisection, setting the working tree to normal.""" |
559 | + working = self._bzrdir.open_workingtree() |
560 | + last_rev = working.branch.last_revision() |
561 | + rev_tree = working.branch.repository.revision_tree(last_rev) |
562 | + working.revert(None, rev_tree, False) |
563 | + if os.path.exists(bisect_rev_path): |
564 | + os.unlink(bisect_rev_path) |
565 | + |
566 | + |
567 | +class BisectLog(object): |
568 | + """Bisect log file handler.""" |
569 | + |
570 | + def __init__(self, filename = bisect_info_path): |
571 | + self._items = [] |
572 | + self._current = BisectCurrent() |
573 | + self._bzrdir = None |
574 | + self._high_revid = None |
575 | + self._low_revid = None |
576 | + self._middle_revid = None |
577 | + self._filename = filename |
578 | + self.load() |
579 | + |
580 | + def _open_for_read(self): |
581 | + """Open log file for reading.""" |
582 | + if self._filename: |
583 | + return open(self._filename) |
584 | + else: |
585 | + return sys.stdin |
586 | + |
587 | + def _open_for_write(self): |
588 | + """Open log file for writing.""" |
589 | + if self._filename: |
590 | + return open(self._filename, "w") |
591 | + else: |
592 | + return sys.stdout |
593 | + |
594 | + def _load_bzr_tree(self): |
595 | + """Load bzr information.""" |
596 | + if not self._bzrdir: |
597 | + self._bzrdir = bzrlib.bzrdir.BzrDir.open_containing('.')[0] |
598 | + self._bzrbranch = self._bzrdir.open_branch() |
599 | + |
600 | + def _find_range_and_middle(self, branch_last_rev = None): |
601 | + """Find the current revision range, and the midpoint.""" |
602 | + self._load_bzr_tree() |
603 | + self._middle_revid = None |
604 | + |
605 | + if not branch_last_rev: |
606 | + last_revid = self._bzrbranch.last_revision() |
607 | + else: |
608 | + last_revid = branch_last_rev |
609 | + |
610 | + repo = self._bzrbranch.repository |
611 | + repo.lock_read() |
612 | + try: |
613 | + rev_sequence = repo.iter_reverse_revision_history(last_revid) |
614 | + high_revid = None |
615 | + low_revid = None |
616 | + between_revs = [] |
617 | + for revision in rev_sequence: |
618 | + between_revs.insert(0, revision) |
619 | + matches = [x[1] for x in self._items |
620 | + if x[0] == revision and x[1] in ('yes', 'no')] |
621 | + if not matches: |
622 | + continue |
623 | + if len(matches) > 1: |
624 | + raise RuntimeError("revision %s duplicated" % revision) |
625 | + if matches[0] == "yes": |
626 | + high_revid = revision |
627 | + between_revs = [] |
628 | + elif matches[0] == "no": |
629 | + low_revid = revision |
630 | + del between_revs[0] |
631 | + break |
632 | + |
633 | + if not high_revid: |
634 | + high_revid = last_revid |
635 | + if not low_revid: |
636 | + low_revid = self._bzrbranch.get_rev_id(1) |
637 | + finally: |
638 | + repo.unlock() |
639 | + |
640 | + # The spread must include the high revision, to bias |
641 | + # odd numbers of intervening revisions towards the high |
642 | + # side. |
643 | + |
644 | + spread = len(between_revs) + 1 |
645 | + if spread < 2: |
646 | + middle_index = 0 |
647 | + else: |
648 | + middle_index = (spread / 2) - 1 |
649 | + |
650 | + if len(between_revs) > 0: |
651 | + self._middle_revid = between_revs[middle_index] |
652 | + else: |
653 | + self._middle_revid = high_revid |
654 | + |
655 | + self._high_revid = high_revid |
656 | + self._low_revid = low_revid |
657 | + |
658 | + def _switch_wc_to_revno(self, revno, outf): |
659 | + """Move the working tree to the given revno.""" |
660 | + self._current.switch(revno) |
661 | + self._current.show_rev_log(out=outf) |
662 | + |
663 | + def _set_status(self, revid, status): |
664 | + """Set the bisect status for the given revid.""" |
665 | + if not self.is_done(): |
666 | + if status != "done" and revid in [x[0] for x in self._items |
667 | + if x[1] in ['yes', 'no']]: |
668 | + raise RuntimeError("attempting to add revid %s twice" % revid) |
669 | + self._items.append((revid, status)) |
670 | + |
671 | + def change_file_name(self, filename): |
672 | + """Switch log files.""" |
673 | + self._filename = filename |
674 | + |
675 | + def load(self): |
676 | + """Load the bisection log.""" |
677 | + self._items = [] |
678 | + if os.path.exists(self._filename): |
679 | + revlog = self._open_for_read() |
680 | + for line in revlog: |
681 | + (revid, status) = line.split() |
682 | + self._items.append((revid, status)) |
683 | + |
684 | + def save(self): |
685 | + """Save the bisection log.""" |
686 | + revlog = self._open_for_write() |
687 | + for (revid, status) in self._items: |
688 | + revlog.write("%s %s\n" % (revid, status)) |
689 | + |
690 | + def is_done(self): |
691 | + """Report whether we've found the right revision.""" |
692 | + return len(self._items) > 0 and self._items[-1][1] == "done" |
693 | + |
694 | + def set_status_from_revspec(self, revspec, status): |
695 | + """Set the bisection status for the revision in revspec.""" |
696 | + self._load_bzr_tree() |
697 | + revid = revspec[0].in_history(self._bzrbranch).rev_id |
698 | + self._set_status(revid, status) |
699 | + |
700 | + def set_current(self, status): |
701 | + """Set the current revision to the given bisection status.""" |
702 | + self._set_status(self._current.get_current_revid(), status) |
703 | + |
704 | + def is_merge_point(self, revid): |
705 | + return len(self.get_parent_revids(revid)) > 1 |
706 | + |
707 | + def get_parent_revids(self, revid): |
708 | + repo = self._bzrbranch.repository |
709 | + repo.lock_read() |
710 | + try: |
711 | + retval = repo.get_parent_map([revid]).get(revid, None) |
712 | + finally: |
713 | + repo.unlock() |
714 | + return retval |
715 | + |
716 | + def bisect(self, outf): |
717 | + """Using the current revision's status, do a bisection.""" |
718 | + self._find_range_and_middle() |
719 | + # If we've found the "final" revision, check for a |
720 | + # merge point. |
721 | + while ((self._middle_revid == self._high_revid |
722 | + or self._middle_revid == self._low_revid) |
723 | + and self.is_merge_point(self._middle_revid)): |
724 | + for parent in self.get_parent_revids(self._middle_revid): |
725 | + if parent == self._low_revid: |
726 | + continue |
727 | + else: |
728 | + self._find_range_and_middle(parent) |
729 | + break |
730 | + self._switch_wc_to_revno(self._middle_revid, outf) |
731 | + if self._middle_revid == self._high_revid or \ |
732 | + self._middle_revid == self._low_revid: |
733 | + self.set_current("done") |
734 | + |
735 | + |
736 | +class cmd_bisect(Command): |
737 | + """Find an interesting commit using a binary search. |
738 | + |
739 | + Bisecting, in a nutshell, is a way to find the commit at which |
740 | + some testable change was made, such as the introduction of a bug |
741 | + or feature. By identifying a version which did not have the |
742 | + interesting change and a later version which did, a developer |
743 | + can test for the presence of the change at various points in |
744 | + the history, eventually ending up at the precise commit when |
745 | + the change was first introduced. |
746 | + |
747 | + This command uses subcommands to implement the search, each |
748 | + of which changes the state of the bisection. The |
749 | + subcommands are: |
750 | + |
751 | + bzr bisect start |
752 | + Start a bisect, possibly clearing out a previous bisect. |
753 | + |
754 | + bzr bisect yes [-r rev] |
755 | + The specified revision (or the current revision, if not given) |
756 | + has the characteristic we're looking for, |
757 | + |
758 | + bzr bisect no [-r rev] |
759 | + The specified revision (or the current revision, if not given) |
760 | + does not have the characteristic we're looking for, |
761 | + |
762 | + bzr bisect move -r rev |
763 | + Switch to a different revision manually. Use if the bisect |
764 | + algorithm chooses a revision that is not suitable. Try to |
765 | + move as little as possible. |
766 | + |
767 | + bzr bisect reset |
768 | + Clear out a bisection in progress. |
769 | + |
770 | + bzr bisect log [-o file] |
771 | + Output a log of the current bisection to standard output, or |
772 | + to the specified file. |
773 | + |
774 | + bzr bisect replay <logfile> |
775 | + Replay a previously-saved bisect log, forgetting any bisection |
776 | + that might be in progress. |
777 | + |
778 | + bzr bisect run <script> |
779 | + Bisect automatically using <script> to determine 'yes' or 'no'. |
780 | + <script> should exit with: |
781 | + 0 for yes |
782 | + 125 for unknown (like build failed so we could not test) |
783 | + anything else for no |
784 | + """ |
785 | + |
786 | + takes_args = ['subcommand', 'args*'] |
787 | + takes_options = [Option('output', short_name='o', |
788 | + help='Write log to this file.', type=unicode), |
789 | + 'revision'] |
790 | + |
791 | + def _check(self): |
792 | + """Check preconditions for most operations to work.""" |
793 | + if not os.path.exists(bisect_info_path): |
794 | + raise BzrCommandError("No bisection in progress.") |
795 | + |
796 | + def _set_state(self, revspec, state): |
797 | + """Set the state of the given revspec and bisecting. |
798 | + |
799 | + Returns boolean indicating if bisection is done.""" |
800 | + bisect_log = BisectLog() |
801 | + if bisect_log.is_done(): |
802 | + note("No further bisection is possible.\n") |
803 | + bisect_log._current.show_rev_log(self.outf) |
804 | + return True |
805 | + |
806 | + if revspec: |
807 | + bisect_log.set_status_from_revspec(revspec, state) |
808 | + else: |
809 | + bisect_log.set_current(state) |
810 | + bisect_log.bisect(self.outf) |
811 | + bisect_log.save() |
812 | + return False |
813 | + |
814 | + def run(self, subcommand, args_list, revision=None, output=None): |
815 | + """Handle the bisect command.""" |
816 | + |
817 | + log_fn = None |
818 | + if subcommand in ('yes', 'no', 'move') and revision: |
819 | + pass |
820 | + elif subcommand in ('replay', ) and args_list and len(args_list) == 1: |
821 | + log_fn = args_list[0] |
822 | + elif subcommand in ('move', ) and not revision: |
823 | + raise BzrCommandError( |
824 | + "The 'bisect move' command requires a revision.") |
825 | + elif subcommand in ('run', ): |
826 | + run_script = args_list[0] |
827 | + elif args_list or revision: |
828 | + raise BzrCommandError( |
829 | + "Improper arguments to bisect " + subcommand) |
830 | + |
831 | + # Dispatch. |
832 | + |
833 | + if subcommand == "start": |
834 | + self.start() |
835 | + elif subcommand == "yes": |
836 | + self.yes(revision) |
837 | + elif subcommand == "no": |
838 | + self.no(revision) |
839 | + elif subcommand == "move": |
840 | + self.move(revision) |
841 | + elif subcommand == "reset": |
842 | + self.reset() |
843 | + elif subcommand == "log": |
844 | + self.log(output) |
845 | + elif subcommand == "replay": |
846 | + self.replay(log_fn) |
847 | + elif subcommand == "run": |
848 | + self.run_bisect(run_script) |
849 | + else: |
850 | + raise BzrCommandError( |
851 | + "Unknown bisect command: " + subcommand) |
852 | + |
853 | + def reset(self): |
854 | + """Reset the bisect state to no state.""" |
855 | + self._check() |
856 | + BisectCurrent().reset() |
857 | + os.unlink(bisect_info_path) |
858 | + |
859 | + def start(self): |
860 | + """Reset the bisect state, then prepare for a new bisection.""" |
861 | + if os.path.exists(bisect_info_path): |
862 | + BisectCurrent().reset() |
863 | + os.unlink(bisect_info_path) |
864 | + |
865 | + bisect_log = BisectLog() |
866 | + bisect_log.set_current("start") |
867 | + bisect_log.save() |
868 | + |
869 | + def yes(self, revspec): |
870 | + """Mark that a given revision has the state we're looking for.""" |
871 | + self._set_state(revspec, "yes") |
872 | + |
873 | + def no(self, revspec): |
874 | + """Mark that a given revision does not have the state we're looking for.""" |
875 | + self._set_state(revspec, "no") |
876 | + |
877 | + def move(self, revspec): |
878 | + """Move to a different revision manually.""" |
879 | + current = BisectCurrent() |
880 | + current.switch(revspec) |
881 | + current.show_rev_log(out=self.outf) |
882 | + |
883 | + def log(self, filename): |
884 | + """Write the current bisect log to a file.""" |
885 | + self._check() |
886 | + bisect_log = BisectLog() |
887 | + bisect_log.change_file_name(filename) |
888 | + bisect_log.save() |
889 | + |
890 | + def replay(self, filename): |
891 | + """Apply the given log file to a clean state, so the state is |
892 | + exactly as it was when the log was saved.""" |
893 | + if os.path.exists(bisect_info_path): |
894 | + BisectCurrent().reset() |
895 | + os.unlink(bisect_info_path) |
896 | + bisect_log = BisectLog(filename) |
897 | + bisect_log.change_file_name(bisect_info_path) |
898 | + bisect_log.save() |
899 | + |
900 | + bisect_log.bisect(self.outf) |
901 | + |
902 | + def run_bisect(self, script): |
903 | + import subprocess |
904 | + note("Starting bisect.") |
905 | + self.start() |
906 | + while True: |
907 | + try: |
908 | + process = subprocess.Popen(script, shell=True) |
909 | + process.wait() |
910 | + retcode = process.returncode |
911 | + if retcode == 0: |
912 | + done = self._set_state(None, 'yes') |
913 | + elif retcode == 125: |
914 | + break |
915 | + else: |
916 | + done = self._set_state(None, 'no') |
917 | + if done: |
918 | + break |
919 | + except RuntimeError: |
920 | + break |
921 | |
922 | === modified file 'tests.py' |
923 | --- tests.py 2010-11-05 12:19:42 +0000 |
924 | +++ tests.py 2011-11-24 16:12:23 +0000 |
925 | @@ -23,16 +23,16 @@ |
926 | import shutil |
927 | |
928 | import bzrlib |
929 | -import bzrlib.bzrdir |
930 | -import bzrlib.tests |
931 | -import bzrlib.revisionspec |
932 | -import bzrlib.plugins.bisect as bisect |
933 | +from bzrlib.bzrdir import BzrDir |
934 | +from bzrlib.plugins import bisect |
935 | +from bzrlib.plugins.bisect import cmds |
936 | from bzrlib.tests import ( |
937 | KnownFailure, |
938 | + TestCaseWithTransport, |
939 | TestSkipped, |
940 | ) |
941 | |
942 | -class BisectTestCase(bzrlib.tests.TestCaseWithTransport): |
943 | +class BisectTestCase(TestCaseWithTransport): |
944 | """Test harness specific to the bisect plugin.""" |
945 | |
946 | def assertRevno(self, rev): |
947 | @@ -57,8 +57,7 @@ |
948 | # a branch from version 1 containing three revisions |
949 | # merged at version 2. |
950 | |
951 | - bzrlib.tests.TestCaseWithTransport.setUp(self) |
952 | - |
953 | + TestCaseWithTransport.setUp(self) |
954 | |
955 | self.tree = self.make_branch_and_tree(".") |
956 | |
957 | @@ -74,8 +73,8 @@ |
958 | 'test_file_append'))) |
959 | self.tree.commit(message = "add test files") |
960 | |
961 | - bzrlib.bzrdir.BzrDir.open(".").sprout("../temp-clone") |
962 | - clone_bzrdir = bzrlib.bzrdir.BzrDir.open("../temp-clone") |
963 | + BzrDir.open(".").sprout("../temp-clone") |
964 | + clone_bzrdir = BzrDir.open("../temp-clone") |
965 | clone_tree = clone_bzrdir.open_workingtree() |
966 | for content in ["one dot one", "one dot two", "one dot three"]: |
967 | test_file = open("../temp-clone/test_file", "w") |
968 | @@ -157,33 +156,33 @@ |
969 | # Not a very good test; just makes sure the code doesn't fail, |
970 | # not that the output makes any sense. |
971 | sio = StringIO() |
972 | - bisect.BisectCurrent().show_rev_log(out=sio) |
973 | + cmds.BisectCurrent().show_rev_log(out=sio) |
974 | |
975 | def testShowLogSubtree(self): |
976 | """Test that a subtree's log can be shown.""" |
977 | - current = bisect.BisectCurrent() |
978 | + current = cmds.BisectCurrent() |
979 | current.switch(self.subtree_rev) |
980 | sio = StringIO() |
981 | current.show_rev_log(out=sio) |
982 | |
983 | def testSwitchVersions(self): |
984 | """Test switching versions.""" |
985 | - current = bisect.BisectCurrent() |
986 | + current = cmds.BisectCurrent() |
987 | self.assertRevno(5) |
988 | current.switch(4) |
989 | self.assertRevno(4) |
990 | |
991 | def testReset(self): |
992 | """Test resetting the working tree to a non-bisected state.""" |
993 | - current = bisect.BisectCurrent() |
994 | + current = cmds.BisectCurrent() |
995 | current.switch(4) |
996 | current.reset() |
997 | self.assertRevno(5) |
998 | - assert not os.path.exists(bisect.bisect_rev_path) |
999 | + assert not os.path.exists(cmds.bisect_rev_path) |
1000 | |
1001 | def testIsMergePoint(self): |
1002 | """Test merge point detection.""" |
1003 | - current = bisect.BisectCurrent() |
1004 | + current = cmds.BisectCurrent() |
1005 | self.assertRevno(5) |
1006 | assert not current.is_merge_point() |
1007 | current.switch(2) |
1008 | @@ -195,17 +194,17 @@ |
1009 | |
1010 | def testCreateBlank(self): |
1011 | """Test creation of new log.""" |
1012 | - bisect_log = bisect.BisectLog() |
1013 | + bisect_log = cmds.BisectLog() |
1014 | bisect_log.save() |
1015 | - assert os.path.exists(bisect.bisect_info_path) |
1016 | + assert os.path.exists(cmds.bisect_info_path) |
1017 | |
1018 | def testLoad(self): |
1019 | """Test loading a log.""" |
1020 | - preloaded_log = open(bisect.bisect_info_path, "w") |
1021 | + preloaded_log = open(cmds.bisect_info_path, "w") |
1022 | preloaded_log.write("rev1 yes\nrev2 no\nrev3 yes\n") |
1023 | preloaded_log.close() |
1024 | |
1025 | - bisect_log = bisect.BisectLog() |
1026 | + bisect_log = cmds.BisectLog() |
1027 | assert len(bisect_log._items) == 3 |
1028 | assert bisect_log._items[0] == ("rev1", "yes") |
1029 | assert bisect_log._items[1] == ("rev2", "no") |
1030 | @@ -213,11 +212,11 @@ |
1031 | |
1032 | def testSave(self): |
1033 | """Test saving the log.""" |
1034 | - bisect_log = bisect.BisectLog() |
1035 | + bisect_log = cmds.BisectLog() |
1036 | bisect_log._items = [("rev1", "yes"), ("rev2", "no"), ("rev3", "yes")] |
1037 | bisect_log.save() |
1038 | |
1039 | - logfile = open(bisect.bisect_info_path) |
1040 | + logfile = open(cmds.bisect_info_path) |
1041 | assert logfile.read() == "rev1 yes\nrev2 no\nrev3 yes\n" |
1042 | |
1043 |
I assume you can copy/paste correctly. Do it! :)