bisect: make bisect a built-in command
authorMatt Mackall <mpm@selenic.com>
Mon, 31 Dec 2007 18:20:34 -0600
changeset 5775 2dd202a6e15b
parent 5774 c850a8640981
child 5776 35ec669cdd43
bisect: make bisect a built-in command
hgext/hbisect.py
mercurial/commands.py
mercurial/hbisect.py
tests/test-bisect
tests/test-debugcomplete.out
tests/test-globalopts.out
tests/test-help.out
--- a/hgext/hbisect.py	Mon Dec 31 18:20:34 2007 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,192 +0,0 @@
-# bisect extension for mercurial
-#
-# Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
-# Inspired by git bisect, extension skeleton taken from mq.py.
-#
-# This software may be used and distributed according to the terms
-# of the GNU General Public License, incorporated herein by reference.
-
-from mercurial.i18n import _
-from mercurial import hg, util, cmdutil
-import os
-
-def _bisect(changelog, state):
-    clparents = changelog.parentrevs
-    # only the earliest bad revision matters
-    badrev = min([changelog.rev(n) for n in state['bad']])
-    bad = changelog.node(badrev)
-    skip = dict.fromkeys([changelog.rev(n) for n in state['skip']])
-
-    # build ancestors array
-    ancestors = [[]] * (changelog.count() + 1) # an extra for [-1]
-
-    # clear good revs from array
-    for node in state['good']:
-        ancestors[changelog.rev(node)] = None
-    for rev in xrange(changelog.count(), -1, -1):
-        if ancestors[rev] is None:
-            for prev in clparents(rev):
-                ancestors[prev] = None
-
-    if ancestors[badrev] is None:
-        raise util.Abort(_("Inconsistent state, %s:%s is good and bad")
-                         % (badrev, hg.short(bad)))
-
-    # build children dict
-    children = {}
-    visit = [badrev]
-    candidates = []
-    while visit:
-        rev = visit.pop(0)
-        if ancestors[rev] == []:
-            candidates.append(rev)
-            for prev in clparents(rev):
-                if prev != -1:
-                    if prev in children:
-                        children[prev].append(rev)
-                    else:
-                        children[prev] = [rev]
-                        visit.append(prev)
-
-    candidates.sort()
-    # have we narrowed it down to one entry?
-    tot = len(candidates)
-    if tot == 1:
-        return (bad, 0)
-    perfect = tot / 2
-
-    # find the best node to test
-    best_rev = None
-    best_len = -1
-    poison = {}
-    for rev in candidates:
-        if rev in poison:
-            for c in children.get(rev, []):
-                poison[c] = True # poison children
-            continue
-
-        a = ancestors[rev] or [rev]
-        ancestors[rev] = None
-
-        x = len(a) # number of ancestors
-        y = tot - x # number of non-ancestors
-        value = min(x, y) # how good is this test?
-        if value > best_len and rev not in skip:
-            best_len = value
-            best_rev = rev
-            if value == perfect: # found a perfect candidate? quit early
-                break
-
-        if y < perfect: # all downhill from here?
-            for c in children.get(rev, []):
-                poison[c] = True # poison children
-            continue
-
-        for c in children.get(rev, []):
-            if ancestors[c]:
-                ancestors[c] = dict.fromkeys(ancestors[c] + a).keys()
-            else:
-                ancestors[c] = a + [c]
-
-    assert best_rev is not None
-    best_node = changelog.node(best_rev)
-
-    return (best_node, tot)
-
-def bisect(ui, repo, rev=None, extra=None,
-               reset=None, good=None, bad=None, skip=None, noupdate=None):
-    """Subdivision search of changesets
-
-This extension helps to find changesets which introduce problems.
-To use, mark the earliest changeset you know exhibits the problem
-as bad, then mark the latest changeset which is free from the problem
-as good. Bisect will update your working directory to a revision for
-testing. Once you have performed tests, mark the working directory
-as bad or good and bisect will either update to another candidate
-changeset or announce that it has found the bad revision.
-
-Note: bisect expects bad revisions to be descendants of good revisions.
-If you are looking for the point at which a problem was fixed, then make
-the problem-free state "bad" and the problematic state "good."
-
-    """
-    # backward compatibility
-    if rev in "good bad reset init".split():
-        ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
-        cmd, rev, extra = rev, extra, None
-        if cmd == "good":
-            good = True
-        elif cmd == "bad":
-            bad = True
-        else:
-            reset = True
-    elif extra or good + bad + skip + reset > 1:
-        raise util.Abort("Incompatible arguments")
-
-    if reset:
-        p = repo.join("bisect.state")
-        if os.path.exists(p):
-            os.unlink(p)
-        return
-
-    # load state
-    state = {'good': [], 'bad': [], 'skip': []}
-    if os.path.exists(repo.join("bisect.state")):
-        for l in repo.opener("bisect.state"):
-            kind, node = l[:-1].split()
-            node = repo.lookup(node)
-            if kind not in state:
-                raise util.Abort(_("unknown bisect kind %s") % kind)
-            state[kind].append(node)
-
-    # update state
-    node = repo.lookup(rev or '.')
-    if good:
-        state['good'].append(node)
-    elif bad:
-        state['bad'].append(node)
-    elif skip:
-        state['skip'].append(node)
-
-    # save state
-    f = repo.opener("bisect.state", "w", atomictemp=True)
-    wlock = repo.wlock()
-    try:
-        for kind in state:
-            for node in state[kind]:
-                f.write("%s %s\n" % (kind, hg.hex(node)))
-        f.rename()
-    finally:
-        del wlock
-
-    if not state['good'] or not state['bad']:
-        return
-
-    # actually bisect
-    node, changesets = _bisect(repo.changelog, state)
-    if changesets == 0:
-        ui.write(_("The first bad revision is:\n"))
-        displayer = cmdutil.show_changeset(ui, repo, {})
-        displayer.show(changenode=node)
-    elif node is not None:
-        # compute the approximate number of remaining tests
-        tests, size = 0, 2
-        while size <= changesets:
-            tests, size = tests + 1, size * 2
-        rev = repo.changelog.rev(node)
-        ui.write(_("Testing changeset %s:%s "
-                   "(%s changesets remaining, ~%s tests)\n")
-                 % (rev, hg.short(node), changesets, tests))
-        if not noupdate:
-            cmdutil.bail_if_changed(repo)
-            return hg.clean(repo, node)
-
-cmdtable = {
-    "bisect": (bisect,
-               [('r', 'reset', False, _('reset bisect state')),
-                ('g', 'good', False, _('mark changeset good')),
-                ('b', 'bad', False, _('mark changeset bad')),
-                ('s', 'skip', False, _('skip testing changeset')),
-                ('U', 'noupdate', False, _('do not update to target'))],
-               _("hg bisect [-gbsr] [REV]"))
-}
--- a/mercurial/commands.py	Mon Dec 31 18:20:34 2007 -0600
+++ b/mercurial/commands.py	Mon Dec 31 18:20:34 2007 -0600
@@ -11,7 +11,7 @@
 import hg, util, revlog, bundlerepo, extensions
 import difflib, patch, time, help, mdiff, tempfile
 import errno, version, socket
-import archival, changegroup, cmdutil, hgweb.server, sshserver
+import archival, changegroup, cmdutil, hgweb.server, sshserver, hbisect
 
 # Commands start here, listed alphabetically
 
@@ -246,6 +246,95 @@
             ui.status(_('(use "backout --merge" '
                         'if you want to auto-merge)\n'))
 
+def bisect(ui, repo, rev=None, extra=None,
+               reset=None, good=None, bad=None, skip=None, noupdate=None):
+    """subdivision search of changesets
+
+    This command helps to find changesets which introduce problems.
+    To use, mark the earliest changeset you know exhibits the problem
+    as bad, then mark the latest changeset which is free from the
+    problem as good. Bisect will update your working directory to a
+    revision for testing. Once you have performed tests, mark the
+    working directory as bad or good and bisect will either update to
+    another candidate changeset or announce that it has found the bad
+    revision.
+
+    Note: bisect expects bad revisions to be descendants of good
+    revisions. If you are looking for the point at which a problem was
+    fixed, then make the problem-free state \"bad\" and the
+    problematic state \"good.\"
+    """
+    # backward compatibility
+    if rev in "good bad reset init".split():
+        ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
+        cmd, rev, extra = rev, extra, None
+        if cmd == "good":
+            good = True
+        elif cmd == "bad":
+            bad = True
+        else:
+            reset = True
+    elif extra or good + bad + skip + reset > 1:
+        raise util.Abort("Incompatible arguments")
+
+    if reset:
+        p = repo.join("bisect.state")
+        if os.path.exists(p):
+            os.unlink(p)
+        return
+
+    # load state
+    state = {'good': [], 'bad': [], 'skip': []}
+    if os.path.exists(repo.join("bisect.state")):
+        for l in repo.opener("bisect.state"):
+            kind, node = l[:-1].split()
+            node = repo.lookup(node)
+            if kind not in state:
+                raise util.Abort(_("unknown bisect kind %s") % kind)
+            state[kind].append(node)
+
+    # update state
+    node = repo.lookup(rev or '.')
+    if good:
+        state['good'].append(node)
+    elif bad:
+        state['bad'].append(node)
+    elif skip:
+        state['skip'].append(node)
+
+    # save state
+    f = repo.opener("bisect.state", "w", atomictemp=True)
+    wlock = repo.wlock()
+    try:
+        for kind in state:
+            for node in state[kind]:
+                f.write("%s %s\n" % (kind, hg.hex(node)))
+        f.rename()
+    finally:
+        del wlock
+
+    if not state['good'] or not state['bad']:
+        return
+
+    # actually bisect
+    node, changesets = hbisect.bisect(repo.changelog, state)
+    if changesets == 0:
+        ui.write(_("The first bad revision is:\n"))
+        displayer = cmdutil.show_changeset(ui, repo, {})
+        displayer.show(changenode=node)
+    elif node is not None:
+        # compute the approximate number of remaining tests
+        tests, size = 0, 2
+        while size <= changesets:
+            tests, size = tests + 1, size * 2
+        rev = repo.changelog.rev(node)
+        ui.write(_("Testing changeset %s:%s "
+                   "(%s changesets remaining, ~%s tests)\n")
+                 % (rev, hg.short(node), changesets, tests))
+        if not noupdate:
+            cmdutil.bail_if_changed(repo)
+            return hg.clean(repo, node)
+
 def branch(ui, repo, label=None, **opts):
     """set or show the current branch name
 
@@ -2658,6 +2747,13 @@
           ('r', 'rev', '', _('revision to backout')),
          ] + walkopts + commitopts + commitopts2,
          _('hg backout [OPTION]... [-r] REV')),
+    "bisect": (bisect,
+               [('r', 'reset', False, _('reset bisect state')),
+                ('g', 'good', False, _('mark changeset good')),
+                ('b', 'bad', False, _('mark changeset bad')),
+                ('s', 'skip', False, _('skip testing changeset')),
+                ('U', 'noupdate', False, _('do not update to target'))],
+               _("hg bisect [-gbsr] [REV]")),
     "branch":
         (branch,
          [('f', 'force', None,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/hbisect.py	Mon Dec 31 18:20:34 2007 -0600
@@ -0,0 +1,94 @@
+# changelog bisection for mercurial
+#
+# Copyright 2007 Matt Mackall
+# Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
+# Inspired by git bisect, extension skeleton taken from mq.py.
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+from i18n import _
+import hg, util
+
+def bisect(changelog, state):
+    clparents = changelog.parentrevs
+    # only the earliest bad revision matters
+    badrev = min([changelog.rev(n) for n in state['bad']])
+    bad = changelog.node(badrev)
+    skip = dict.fromkeys([changelog.rev(n) for n in state['skip']])
+
+    # build ancestors array
+    ancestors = [[]] * (changelog.count() + 1) # an extra for [-1]
+
+    # clear good revs from array
+    for node in state['good']:
+        ancestors[changelog.rev(node)] = None
+    for rev in xrange(changelog.count(), -1, -1):
+        if ancestors[rev] is None:
+            for prev in clparents(rev):
+                ancestors[prev] = None
+
+    if ancestors[badrev] is None:
+        raise util.Abort(_("Inconsistent state, %s:%s is good and bad")
+                         % (badrev, hg.short(bad)))
+
+    # build children dict
+    children = {}
+    visit = [badrev]
+    candidates = []
+    while visit:
+        rev = visit.pop(0)
+        if ancestors[rev] == []:
+            candidates.append(rev)
+            for prev in clparents(rev):
+                if prev != -1:
+                    if prev in children:
+                        children[prev].append(rev)
+                    else:
+                        children[prev] = [rev]
+                        visit.append(prev)
+
+    candidates.sort()
+    # have we narrowed it down to one entry?
+    tot = len(candidates)
+    if tot == 1:
+        return (bad, 0)
+    perfect = tot / 2
+
+    # find the best node to test
+    best_rev = None
+    best_len = -1
+    poison = {}
+    for rev in candidates:
+        if rev in poison:
+            for c in children.get(rev, []):
+                poison[c] = True # poison children
+            continue
+
+        a = ancestors[rev] or [rev]
+        ancestors[rev] = None
+
+        x = len(a) # number of ancestors
+        y = tot - x # number of non-ancestors
+        value = min(x, y) # how good is this test?
+        if value > best_len and rev not in skip:
+            best_len = value
+            best_rev = rev
+            if value == perfect: # found a perfect candidate? quit early
+                break
+
+        if y < perfect: # all downhill from here?
+            for c in children.get(rev, []):
+                poison[c] = True # poison children
+            continue
+
+        for c in children.get(rev, []):
+            if ancestors[c]:
+                ancestors[c] = dict.fromkeys(ancestors[c] + a).keys()
+            else:
+                ancestors[c] = a + [c]
+
+    assert best_rev is not None
+    best_node = changelog.node(best_rev)
+
+    return (best_node, tot)
--- a/tests/test-bisect	Mon Dec 31 18:20:34 2007 -0600
+++ b/tests/test-bisect	Mon Dec 31 18:20:34 2007 -0600
@@ -2,9 +2,6 @@
 
 set -e
 
-echo "[extensions]" >> $HGRCPATH
-echo "hbisect=" >> $HGRCPATH
-
 echo % init
 hg init
 
--- a/tests/test-debugcomplete.out	Mon Dec 31 18:20:34 2007 -0600
+++ b/tests/test-debugcomplete.out	Mon Dec 31 18:20:34 2007 -0600
@@ -4,6 +4,7 @@
 annotate
 archive
 backout
+bisect
 branch
 branches
 bundle
--- a/tests/test-globalopts.out	Mon Dec 31 18:20:34 2007 -0600
+++ b/tests/test-globalopts.out	Mon Dec 31 18:20:34 2007 -0600
@@ -147,6 +147,7 @@
  annotate     show changeset information per file line
  archive      create unversioned archive of a repository revision
  backout      reverse effect of earlier changeset
+ bisect       subdivision search of changesets
  branch       set or show the current branch name
  branches     list repository named branches
  bundle       create a changegroup file
@@ -199,6 +200,7 @@
  annotate     show changeset information per file line
  archive      create unversioned archive of a repository revision
  backout      reverse effect of earlier changeset
+ bisect       subdivision search of changesets
  branch       set or show the current branch name
  branches     list repository named branches
  bundle       create a changegroup file
--- a/tests/test-help.out	Mon Dec 31 18:20:34 2007 -0600
+++ b/tests/test-help.out	Mon Dec 31 18:20:34 2007 -0600
@@ -45,6 +45,7 @@
  annotate     show changeset information per file line
  archive      create unversioned archive of a repository revision
  backout      reverse effect of earlier changeset
+ bisect       subdivision search of changesets
  branch       set or show the current branch name
  branches     list repository named branches
  bundle       create a changegroup file
@@ -93,6 +94,7 @@
  annotate     show changeset information per file line
  archive      create unversioned archive of a repository revision
  backout      reverse effect of earlier changeset
+ bisect       subdivision search of changesets
  branch       set or show the current branch name
  branches     list repository named branches
  bundle       create a changegroup file