[SCM] WebKit Debian packaging branch, webkit-1.2, updated. upstream/1.1.90-6072-g9a69373

eric at webkit.org eric at webkit.org
Thu Apr 8 00:52:59 UTC 2010


The following commit has been merged in the webkit-1.2 branch:
commit 81e8a766e80249cf780316f58d65f1a43396aab7
Author: eric at webkit.org <eric at webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Date:   Mon Jan 4 07:04:57 2010 +0000

    2010-01-03  Eric Seidel  <eric at webkit.org>
    
            Reviewed by Adam Barth.
    
            Rename Scripts/modules to Scripts/webkitpy
            https://bugs.webkit.org/show_bug.cgi?id=33128
    
            Just search-replace and svn mv commands.
    
            * Scripts/bugzilla-tool:
            * Scripts/check-webkit-style:
            * Scripts/modules: Removed.
            * Scripts/modules/BeautifulSoup.py: Removed.
            * Scripts/modules/__init__.py: Removed.
            * Scripts/modules/bugzilla.py: Removed.
            * Scripts/modules/bugzilla_unittest.py: Removed.
            * Scripts/modules/buildbot.py: Removed.
            * Scripts/modules/buildbot_unittest.py: Removed.
            * Scripts/modules/buildsteps.py: Removed.
            * Scripts/modules/buildsteps_unittest.py: Removed.
            * Scripts/modules/changelogs.py: Removed.
            * Scripts/modules/changelogs_unittest.py: Removed.
            * Scripts/modules/commands: Removed.
            * Scripts/modules/commands/__init__.py: Removed.
            * Scripts/modules/commands/commandtest.py: Removed.
            * Scripts/modules/commands/download.py: Removed.
            * Scripts/modules/commands/download_unittest.py: Removed.
            * Scripts/modules/commands/early_warning_system.py: Removed.
            * Scripts/modules/commands/early_warning_system_unittest.py: Removed.
            * Scripts/modules/commands/queries.py: Removed.
            * Scripts/modules/commands/queries_unittest.py: Removed.
            * Scripts/modules/commands/queues.py: Removed.
            * Scripts/modules/commands/queues_unittest.py: Removed.
            * Scripts/modules/commands/queuestest.py: Removed.
            * Scripts/modules/commands/upload.py: Removed.
            * Scripts/modules/commands/upload_unittest.py: Removed.
            * Scripts/modules/comments.py: Removed.
            * Scripts/modules/committers.py: Removed.
            * Scripts/modules/committers_unittest.py: Removed.
            * Scripts/modules/cpp_style.py: Removed.
            * Scripts/modules/cpp_style_unittest.py: Removed.
            * Scripts/modules/credentials.py: Removed.
            * Scripts/modules/credentials_unittest.py: Removed.
            * Scripts/modules/diff_parser.py: Removed.
            * Scripts/modules/diff_parser_unittest.py: Removed.
            * Scripts/modules/executive.py: Removed.
            * Scripts/modules/executive_unittest.py: Removed.
            * Scripts/modules/grammar.py: Removed.
            * Scripts/modules/mock.py: Removed.
            * Scripts/modules/mock_bugzillatool.py: Removed.
            * Scripts/modules/multicommandtool.py: Removed.
            * Scripts/modules/multicommandtool_unittest.py: Removed.
            * Scripts/modules/outputcapture.py: Removed.
            * Scripts/modules/patchcollection.py: Removed.
            * Scripts/modules/queueengine.py: Removed.
            * Scripts/modules/queueengine_unittest.py: Removed.
            * Scripts/modules/scm.py: Removed.
            * Scripts/modules/scm_unittest.py: Removed.
            * Scripts/modules/statusbot.py: Removed.
            * Scripts/modules/stepsequence.py: Removed.
            * Scripts/modules/style: Removed.
            * Scripts/modules/style.py: Removed.
            * Scripts/modules/style_unittest.py: Removed.
            * Scripts/modules/text_style.py: Removed.
            * Scripts/modules/text_style_unittest.py: Removed.
            * Scripts/modules/user.py: Removed.
            * Scripts/modules/webkit_logging.py: Removed.
            * Scripts/modules/webkit_logging_unittest.py: Removed.
            * Scripts/modules/webkit_mechanize.py: Removed.
            * Scripts/modules/webkitport.py: Removed.
            * Scripts/modules/webkitport_unittest.py: Removed.
            * Scripts/test-webkit-python: Removed.
            * Scripts/test-webkitpy: Copied from WebKitTools/Scripts/test-webkit-python.
            * Scripts/validate-committer-lists:
            * Scripts/webkitpy: Copied from WebKitTools/Scripts/modules.
            * Scripts/webkitpy/bugzilla.py:
            * Scripts/webkitpy/bugzilla_unittest.py:
            * Scripts/webkitpy/buildbot.py:
            * Scripts/webkitpy/buildbot_unittest.py:
            * Scripts/webkitpy/buildsteps.py:
            * Scripts/webkitpy/buildsteps_unittest.py:
            * Scripts/webkitpy/commands/commandtest.py:
            * Scripts/webkitpy/commands/download.py:
            * Scripts/webkitpy/commands/download_unittest.py:
            * Scripts/webkitpy/commands/early_warning_system.py:
            * Scripts/webkitpy/commands/early_warning_system_unittest.py:
            * Scripts/webkitpy/commands/queries.py:
            * Scripts/webkitpy/commands/queries_unittest.py:
            * Scripts/webkitpy/commands/queues.py:
            * Scripts/webkitpy/commands/queues_unittest.py:
            * Scripts/webkitpy/commands/queuestest.py:
            * Scripts/webkitpy/commands/upload.py:
            * Scripts/webkitpy/commands/upload_unittest.py:
            * Scripts/webkitpy/comments.py:
            * Scripts/webkitpy/credentials.py:
            * Scripts/webkitpy/credentials_unittest.py:
            * Scripts/webkitpy/executive.py:
            * Scripts/webkitpy/executive_unittest.py:
            * Scripts/webkitpy/mock_bugzillatool.py:
            * Scripts/webkitpy/multicommandtool.py:
            * Scripts/webkitpy/multicommandtool_unittest.py:
            * Scripts/webkitpy/queueengine.py:
            * Scripts/webkitpy/queueengine_unittest.py:
            * Scripts/webkitpy/scm.py:
            * Scripts/webkitpy/scm_unittest.py:
            * Scripts/webkitpy/statusbot.py:
            * Scripts/webkitpy/stepsequence.py:
            * Scripts/webkitpy/webkit_logging_unittest.py:
            * Scripts/webkitpy/webkitport_unittest.py:
    
    
    git-svn-id: http://svn.webkit.org/repository/webkit/trunk@52703 268f45cc-cd09-0410-ab3c-d52691b4dbfc

diff --git a/WebKitTools/ChangeLog b/WebKitTools/ChangeLog
index eed06c3..74cf749 100644
--- a/WebKitTools/ChangeLog
+++ b/WebKitTools/ChangeLog
@@ -1,3 +1,113 @@
+2010-01-03  Eric Seidel  <eric at webkit.org>
+
+        Reviewed by Adam Barth.
+
+        Rename Scripts/modules to Scripts/webkitpy
+        https://bugs.webkit.org/show_bug.cgi?id=33128
+
+        Just search-replace and svn mv commands.
+
+        * Scripts/bugzilla-tool:
+        * Scripts/check-webkit-style:
+        * Scripts/modules: Removed.
+        * Scripts/modules/BeautifulSoup.py: Removed.
+        * Scripts/modules/__init__.py: Removed.
+        * Scripts/modules/bugzilla.py: Removed.
+        * Scripts/modules/bugzilla_unittest.py: Removed.
+        * Scripts/modules/buildbot.py: Removed.
+        * Scripts/modules/buildbot_unittest.py: Removed.
+        * Scripts/modules/buildsteps.py: Removed.
+        * Scripts/modules/buildsteps_unittest.py: Removed.
+        * Scripts/modules/changelogs.py: Removed.
+        * Scripts/modules/changelogs_unittest.py: Removed.
+        * Scripts/modules/commands: Removed.
+        * Scripts/modules/commands/__init__.py: Removed.
+        * Scripts/modules/commands/commandtest.py: Removed.
+        * Scripts/modules/commands/download.py: Removed.
+        * Scripts/modules/commands/download_unittest.py: Removed.
+        * Scripts/modules/commands/early_warning_system.py: Removed.
+        * Scripts/modules/commands/early_warning_system_unittest.py: Removed.
+        * Scripts/modules/commands/queries.py: Removed.
+        * Scripts/modules/commands/queries_unittest.py: Removed.
+        * Scripts/modules/commands/queues.py: Removed.
+        * Scripts/modules/commands/queues_unittest.py: Removed.
+        * Scripts/modules/commands/queuestest.py: Removed.
+        * Scripts/modules/commands/upload.py: Removed.
+        * Scripts/modules/commands/upload_unittest.py: Removed.
+        * Scripts/modules/comments.py: Removed.
+        * Scripts/modules/committers.py: Removed.
+        * Scripts/modules/committers_unittest.py: Removed.
+        * Scripts/modules/cpp_style.py: Removed.
+        * Scripts/modules/cpp_style_unittest.py: Removed.
+        * Scripts/modules/credentials.py: Removed.
+        * Scripts/modules/credentials_unittest.py: Removed.
+        * Scripts/modules/diff_parser.py: Removed.
+        * Scripts/modules/diff_parser_unittest.py: Removed.
+        * Scripts/modules/executive.py: Removed.
+        * Scripts/modules/executive_unittest.py: Removed.
+        * Scripts/modules/grammar.py: Removed.
+        * Scripts/modules/mock.py: Removed.
+        * Scripts/modules/mock_bugzillatool.py: Removed.
+        * Scripts/modules/multicommandtool.py: Removed.
+        * Scripts/modules/multicommandtool_unittest.py: Removed.
+        * Scripts/modules/outputcapture.py: Removed.
+        * Scripts/modules/patchcollection.py: Removed.
+        * Scripts/modules/queueengine.py: Removed.
+        * Scripts/modules/queueengine_unittest.py: Removed.
+        * Scripts/modules/scm.py: Removed.
+        * Scripts/modules/scm_unittest.py: Removed.
+        * Scripts/modules/statusbot.py: Removed.
+        * Scripts/modules/stepsequence.py: Removed.
+        * Scripts/modules/style: Removed.
+        * Scripts/modules/style.py: Removed.
+        * Scripts/modules/style_unittest.py: Removed.
+        * Scripts/modules/text_style.py: Removed.
+        * Scripts/modules/text_style_unittest.py: Removed.
+        * Scripts/modules/user.py: Removed.
+        * Scripts/modules/webkit_logging.py: Removed.
+        * Scripts/modules/webkit_logging_unittest.py: Removed.
+        * Scripts/modules/webkit_mechanize.py: Removed.
+        * Scripts/modules/webkitport.py: Removed.
+        * Scripts/modules/webkitport_unittest.py: Removed.
+        * Scripts/test-webkit-python: Removed.
+        * Scripts/test-webkitpy: Copied from WebKitTools/Scripts/test-webkit-python.
+        * Scripts/validate-committer-lists:
+        * Scripts/webkitpy: Copied from WebKitTools/Scripts/modules.
+        * Scripts/webkitpy/bugzilla.py:
+        * Scripts/webkitpy/bugzilla_unittest.py:
+        * Scripts/webkitpy/buildbot.py:
+        * Scripts/webkitpy/buildbot_unittest.py:
+        * Scripts/webkitpy/buildsteps.py:
+        * Scripts/webkitpy/buildsteps_unittest.py:
+        * Scripts/webkitpy/commands/commandtest.py:
+        * Scripts/webkitpy/commands/download.py:
+        * Scripts/webkitpy/commands/download_unittest.py:
+        * Scripts/webkitpy/commands/early_warning_system.py:
+        * Scripts/webkitpy/commands/early_warning_system_unittest.py:
+        * Scripts/webkitpy/commands/queries.py:
+        * Scripts/webkitpy/commands/queries_unittest.py:
+        * Scripts/webkitpy/commands/queues.py:
+        * Scripts/webkitpy/commands/queues_unittest.py:
+        * Scripts/webkitpy/commands/queuestest.py:
+        * Scripts/webkitpy/commands/upload.py:
+        * Scripts/webkitpy/commands/upload_unittest.py:
+        * Scripts/webkitpy/comments.py:
+        * Scripts/webkitpy/credentials.py:
+        * Scripts/webkitpy/credentials_unittest.py:
+        * Scripts/webkitpy/executive.py:
+        * Scripts/webkitpy/executive_unittest.py:
+        * Scripts/webkitpy/mock_bugzillatool.py:
+        * Scripts/webkitpy/multicommandtool.py:
+        * Scripts/webkitpy/multicommandtool_unittest.py:
+        * Scripts/webkitpy/queueengine.py:
+        * Scripts/webkitpy/queueengine_unittest.py:
+        * Scripts/webkitpy/scm.py:
+        * Scripts/webkitpy/scm_unittest.py:
+        * Scripts/webkitpy/statusbot.py:
+        * Scripts/webkitpy/stepsequence.py:
+        * Scripts/webkitpy/webkit_logging_unittest.py:
+        * Scripts/webkitpy/webkitport_unittest.py:
+
 2010-01-03  Chris Jerdonek  <chris.jerdonek at gmail.com>
 
         Reviewed by Eric Seidel.
diff --git a/WebKitTools/Scripts/bugzilla-tool b/WebKitTools/Scripts/bugzilla-tool
index 9305b66..c8a52e9 100755
--- a/WebKitTools/Scripts/bugzilla-tool
+++ b/WebKitTools/Scripts/bugzilla-tool
@@ -32,18 +32,18 @@
 
 import os
 
-from modules.bugzilla import Bugzilla
-from modules.buildbot import BuildBot
-from modules.commands.download import *
-from modules.commands.early_warning_system import *
-from modules.commands.queries import *
-from modules.commands.queues import *
-from modules.commands.upload import *
-from modules.executive import Executive
-from modules.webkit_logging import log
-from modules.multicommandtool import MultiCommandTool
-from modules.scm import detect_scm_system
-from modules.user import User
+from webkitpy.bugzilla import Bugzilla
+from webkitpy.buildbot import BuildBot
+from webkitpy.commands.download import *
+from webkitpy.commands.early_warning_system import *
+from webkitpy.commands.queries import *
+from webkitpy.commands.queues import *
+from webkitpy.commands.upload import *
+from webkitpy.executive import Executive
+from webkitpy.webkit_logging import log
+from webkitpy.multicommandtool import MultiCommandTool
+from webkitpy.scm import detect_scm_system
+from webkitpy.user import User
 
 
 class BugzillaTool(MultiCommandTool):
diff --git a/WebKitTools/Scripts/check-webkit-style b/WebKitTools/Scripts/check-webkit-style
index 6634854..08983d8 100755
--- a/WebKitTools/Scripts/check-webkit-style
+++ b/WebKitTools/Scripts/check-webkit-style
@@ -46,8 +46,8 @@ import os
 import os.path
 import sys
 
-import modules.style as style
-from modules.scm import detect_scm_system
+import webkitpy.style as style
+from webkitpy.scm import detect_scm_system
 
 
 def main():
diff --git a/WebKitTools/Scripts/modules/bugzilla.py b/WebKitTools/Scripts/modules/bugzilla.py
deleted file mode 100644
index e6536a6..0000000
--- a/WebKitTools/Scripts/modules/bugzilla.py
+++ /dev/null
@@ -1,563 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# WebKit's Python module for interacting with Bugzilla
-
-import re
-import subprocess
-
-from datetime import datetime # used in timestamp()
-
-# Import WebKit-specific modules.
-from modules.webkit_logging import error, log
-from modules.committers import CommitterList
-from modules.credentials import Credentials
-
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
-# so this import should always succeed.
-from .BeautifulSoup import BeautifulSoup, SoupStrainer
-
-from modules.webkit_mechanize import Browser
-
-def parse_bug_id(message):
-    match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", message)
-    if match:
-        return int(match.group('bug_id'))
-    match = re.search(Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)", message)
-    if match:
-        return int(match.group('bug_id'))
-    return None
-
-
-def timestamp():
-    return datetime.now().strftime("%Y%m%d%H%M%S")
-
-
-# FIXME: This class is kinda a hack for now.  It exists so we have one place
-# to hold bug logic, even if much of the code deals with dictionaries still.
-class Bug(object):
-    def __init__(self, bug_dictionary):
-        self.bug_dictionary = bug_dictionary
-
-    def assigned_to_email(self):
-        return self.bug_dictionary["assigned_to_email"]
-
-    # Rarely do we actually want obsolete attachments
-    def attachments(self, include_obsolete=False):
-        if include_obsolete:
-            return self.bug_dictionary["attachments"][:] # Return a copy in either case.
-        return [attachment for attachment in self.bug_dictionary["attachments"] if not attachment["is_obsolete"]]
-
-    def patches(self, include_obsolete=False):
-        return [patch for patch in self.attachments(include_obsolete) if patch["is_patch"]]
-
-    def unreviewed_patches(self):
-        return [patch for patch in self.patches() if patch.get("review") == "?"]
-
-
-# A container for all of the logic for making a parsing buzilla queries.
-class BugzillaQueries(object):
-    def __init__(self, bugzilla):
-        self.bugzilla = bugzilla
-
-    def _load_query(self, query):
-        full_url = "%s%s" % (self.bugzilla.bug_server_url, query)
-        return self.bugzilla.browser.open(full_url)
-
-    def _fetch_bug_ids_advanced_query(self, query):
-        soup = BeautifulSoup(self._load_query(query))
-        # The contents of the <a> inside the cells in the first column happen to be the bug id.
-        return [int(bug_link_cell.find("a").string) for bug_link_cell in soup('td', "first-child")]
-
-    def _parse_attachment_ids_request_query(self, page):
-        digits = re.compile("\d+")
-        attachment_href = re.compile("attachment.cgi\?id=\d+&action=review")
-        attachment_links = SoupStrainer("a", href=attachment_href)
-        return [int(digits.search(tag["href"]).group(0)) for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)]
-
-    def _fetch_attachment_ids_request_query(self, query):
-        return self._parse_attachment_ids_request_query(self._load_query(query))
-
-    # List of all r+'d bugs.
-    def fetch_bug_ids_from_pending_commit_list(self):
-        needs_commit_query_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review%2B"
-        return self._fetch_bug_ids_advanced_query(needs_commit_query_url)
-
-    def fetch_patches_from_pending_commit_list(self):
-        # FIXME: This should not have to go through self.bugzilla
-        return sum([self.bugzilla.fetch_reviewed_patches_from_bug(bug_id) for bug_id in self.fetch_bug_ids_from_pending_commit_list()], [])
-
-    def fetch_bug_ids_from_commit_queue(self):
-        commit_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=commit-queue%2B"
-        return self._fetch_bug_ids_advanced_query(commit_queue_url)
-
-    def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
-        # FIXME: Once reject_invalid_patches is moved out of this function this becomes a simple list comprehension using fetch_bug_ids_from_commit_queue.
-        patches_to_land = []
-        for bug_id in self.fetch_bug_ids_from_commit_queue():
-            # FIXME: This should not have to go through self.bugzilla
-            patches = self.bugzilla.fetch_commit_queue_patches_from_bug(bug_id, reject_invalid_patches)
-            patches_to_land += patches
-        return patches_to_land
-
-    def _fetch_bug_ids_from_review_queue(self):
-        review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?"
-        return self._fetch_bug_ids_advanced_query(review_queue_url)
-
-    def fetch_patches_from_review_queue(self, limit=None):
-        # FIXME: We should probably have a self.fetch_bug to minimize the number of self.bugzilla calls.
-        return sum([self.bugzilla.fetch_bug(bug_id).unreviewed_patches() for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) # [:None] returns the whole array.
-
-    # FIXME: Why do we have both fetch_patches_from_review_queue and fetch_attachment_ids_from_review_queue??
-    # NOTE: This is also the only client of _fetch_attachment_ids_request_query
-    def fetch_attachment_ids_from_review_queue(self):
-        review_queue_url = "request.cgi?action=queue&type=review&group=type"
-        return self._fetch_attachment_ids_request_query(review_queue_url)
-
-
-class Bugzilla(object):
-    def __init__(self, dryrun=False, committers=CommitterList()):
-        self.dryrun = dryrun
-        self.authenticated = False
-        self.queries = BugzillaQueries(self)
-
-        # FIXME: We should use some sort of Browser mock object when in dryrun mode (to prevent any mistakes).
-        self.browser = Browser()
-        # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script
-        self.browser.set_handle_robots(False)
-        self.committers = committers
-
-    # FIXME: Much of this should go into some sort of config module:
-    bug_server_host = "bugs.webkit.org"
-    bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host)
-    bug_server_url = "https://%s/" % bug_server_host
-    unassigned_email = "webkit-unassigned at lists.webkit.org"
-
-    def bug_url_for_bug_id(self, bug_id, xml=False):
-        content_type = "&ctype=xml" if xml else ""
-        return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url, bug_id, content_type)
-
-    def short_bug_url_for_bug_id(self, bug_id):
-        return "http://webkit.org/b/%s" % bug_id
-
-    def attachment_url_for_id(self, attachment_id, action="view"):
-        action_param = ""
-        if action and action != "view":
-            action_param = "&action=%s" % action
-        return "%sattachment.cgi?id=%s%s" % (self.bug_server_url, attachment_id, action_param)
-
-    def _parse_attachment_flag(self, element, flag_name, attachment, result_key):
-        flag = element.find('flag', attrs={'name' : flag_name})
-        if flag:
-            attachment[flag_name] = flag['status']
-            if flag['status'] == '+':
-                attachment[result_key] = flag['setter']
-
-    def _parse_attachment_element(self, element, bug_id):
-        attachment = {}
-        attachment['bug_id'] = bug_id
-        attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
-        attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
-        attachment['id'] = int(element.find('attachid').string)
-        attachment['url'] = self.attachment_url_for_id(attachment['id'])
-        attachment['name'] = unicode(element.find('desc').string)
-        attachment['attacher_email'] = str(element.find('attacher').string)
-        attachment['type'] = str(element.find('type').string)
-        self._parse_attachment_flag(element, 'review', attachment, 'reviewer_email')
-        self._parse_attachment_flag(element, 'commit-queue', attachment, 'committer_email')
-        return attachment
-
-    def _parse_bug_page(self, page):
-        soup = BeautifulSoup(page)
-        bug = {}
-        bug["id"] = int(soup.find("bug_id").string)
-        bug["title"] = unicode(soup.find("short_desc").string)
-        bug["reporter_email"] = str(soup.find("reporter").string)
-        bug["assigned_to_email"] = str(soup.find("assigned_to").string)
-        bug["cc_emails"] = [str(element.string) for element in soup.findAll('cc')]
-        bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
-        return bug
-
-    # Makes testing fetch_*_from_bug() possible until we have a better BugzillaNetwork abstration.
-    def _fetch_bug_page(self, bug_id):
-        bug_url = self.bug_url_for_bug_id(bug_id, xml=True)
-        log("Fetching: %s" % bug_url)
-        return self.browser.open(bug_url)
-
-    def fetch_bug_dictionary(self, bug_id):
-        return self._parse_bug_page(self._fetch_bug_page(bug_id))
-
-    # FIXME: A BugzillaCache object should provide all these fetch_ methods.
-    def fetch_bug(self, bug_id):
-        return Bug(self.fetch_bug_dictionary(bug_id))
-
-    def _parse_bug_id_from_attachment_page(self, page):
-        up_link = BeautifulSoup(page).find('link', rel='Up') # The "Up" relation happens to point to the bug.
-        if not up_link:
-            return None # This attachment does not exist (or you don't have permissions to view it).
-        match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href'])
-        return int(match.group('bug_id'))
-
-    def bug_id_for_attachment_id(self, attachment_id):
-        attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
-        log("Fetching: %s" % attachment_url)
-        page = self.browser.open(attachment_url)
-        return self._parse_bug_id_from_attachment_page(page)
-
-    # This should really return an Attachment object
-    # which can lazily fetch any missing data.
-    def fetch_attachment(self, attachment_id):
-        # We could grab all the attachment details off of the attachment edit page
-        # but we already have working code to do so off of the bugs page, so re-use that.
-        bug_id = self.bug_id_for_attachment_id(attachment_id)
-        if not bug_id:
-            return None
-        for attachment in self.fetch_bug(bug_id).attachments(include_obsolete=True):
-            # FIXME: Once we have a real Attachment class we shouldn't paper over this possible comparison failure
-            # and we should remove the int() == int() hacks and leave it just ==.
-            if int(attachment['id']) == int(attachment_id):
-                self._validate_committer_and_reviewer(attachment)
-                return attachment
-        return None # This should never be hit.
-
-    # fetch_patches_from_bug exists until we expose a Bug class outside of bugzilla.py
-    def fetch_patches_from_bug(self, bug_id):
-        return self.fetch_bug(bug_id).patches()
-
-    # _view_source_link belongs in some sort of webkit_config.py module.
-    def _view_source_link(self, local_path):
-        return "http://trac.webkit.org/browser/trunk/%s" % local_path
-
-    def _flag_permission_rejection_message(self, setter_email, flag_name):
-        committer_list = "WebKitTools/Scripts/modules/committers.py" # This could be computed from CommitterList.__file__
-        contribution_guidlines_url = "http://webkit.org/coding/contributing.html" # Should come from some webkit_config.py
-        queue_administrator = "eseidel at chromium.org" # This could be queried from the status_bot.
-        queue_name = "commit-queue" # This could be queried from the tool.
-        rejection_message = "%s does not have %s permissions according to %s." % (setter_email, flag_name, self._view_source_link(committer_list))
-        rejection_message += "\n\n- If you do not have %s rights please read %s for instructions on how to use bugzilla flags." % (flag_name, contribution_guidlines_url)
-        rejection_message += "\n\n- If you have %s rights please correct the error in %s by adding yourself to the file (no review needed)." % (flag_name, committer_list)
-        rejection_message += "  Due to bug 30084 the %s will require a restart after your change." % queue_name
-        rejection_message += "  Please contact %s to request a %s restart." % (queue_administrator, queue_name)
-        rejection_message += "  After restart the %s will correctly respect your %s rights." % (queue_name, flag_name)
-        return rejection_message
-
-    def _validate_setter_email(self, patch, result_key, lookup_function, rejection_function, reject_invalid_patches):
-        setter_email = patch.get(result_key + '_email')
-        if not setter_email:
-            return None
-
-        committer = lookup_function(setter_email)
-        if committer:
-            patch[result_key] = committer.full_name
-            return patch[result_key]
-
-        if reject_invalid_patches:
-            rejection_function(patch['id'], self._flag_permission_rejection_message(setter_email, result_key))
-        else:
-            log("Warning, attachment %s on bug %s has invalid %s (%s)" % (patch['id'], patch['bug_id'], result_key, setter_email))
-        return None
-
-    def _validate_reviewer(self, patch, reject_invalid_patches):
-        return self._validate_setter_email(patch, 'reviewer', self.committers.reviewer_by_email, self.reject_patch_from_review_queue, reject_invalid_patches)
-
-    def _validate_committer(self, patch, reject_invalid_patches):
-        return self._validate_setter_email(patch, 'committer', self.committers.committer_by_email, self.reject_patch_from_commit_queue, reject_invalid_patches)
-
-    # FIXME: This is a hack until we have a real Attachment object.
-    # _validate_committer and _validate_reviewer fill in the 'reviewer' and 'committer'
-    # keys which other parts of the code expect to be filled in.
-    def _validate_committer_and_reviewer(self, patch):
-        self._validate_reviewer(patch, reject_invalid_patches=False)
-        self._validate_committer(patch, reject_invalid_patches=False)
-
-    # FIXME: fetch_reviewed_patches_from_bug and fetch_commit_queue_patches_from_bug
-    # should share more code and use list comprehensions.
-    def fetch_reviewed_patches_from_bug(self, bug_id, reject_invalid_patches=False):
-        reviewed_patches = []
-        for attachment in self.fetch_bug(bug_id).attachments():
-            if self._validate_reviewer(attachment, reject_invalid_patches):
-                reviewed_patches.append(attachment)
-        return reviewed_patches
-
-    def fetch_commit_queue_patches_from_bug(self, bug_id, reject_invalid_patches=False):
-        commit_queue_patches = []
-        for attachment in self.fetch_reviewed_patches_from_bug(bug_id, reject_invalid_patches):
-            if self._validate_committer(attachment, reject_invalid_patches):
-                commit_queue_patches.append(attachment)
-        return commit_queue_patches
-
-    def authenticate(self):
-        if self.authenticated:
-            return
-
-        if self.dryrun:
-            log("Skipping log in for dry run...")
-            self.authenticated = True
-            return
-
-        (username, password) = Credentials(self.bug_server_host, git_prefix="bugzilla").read_credentials()
-
-        log("Logging in as %s..." % username)
-        self.browser.open(self.bug_server_url + "index.cgi?GoAheadAndLogIn=1")
-        self.browser.select_form(name="login")
-        self.browser['Bugzilla_login'] = username
-        self.browser['Bugzilla_password'] = password
-        response = self.browser.submit()
-
-        match = re.search("<title>(.+?)</title>", response.read())
-        # If the resulting page has a title, and it contains the word "invalid" assume it's the login failure page.
-        if match and re.search("Invalid", match.group(1), re.IGNORECASE):
-            # FIXME: We could add the ability to try again on failure.
-            raise Exception("Bugzilla login failed: %s" % match.group(1))
-
-        self.authenticated = True
-
-    def _fill_attachment_form(self, description, patch_file_object, comment_text=None, mark_for_review=False, mark_for_commit_queue=False, bug_id=None):
-        self.browser['description'] = description
-        self.browser['ispatch'] = ("1",)
-        self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',)
-        self.browser['flag_type-3'] = ('?',) if mark_for_commit_queue else ('X',)
-        if bug_id:
-            patch_name = "bug-%s-%s.patch" % (bug_id, timestamp())
-        else:
-            patch_name ="%s.patch" % timestamp()
-        self.browser.add_file(patch_file_object, "text/plain", patch_name, 'data')
-
-    def add_patch_to_bug(self, bug_id, patch_file_object, description, comment_text=None, mark_for_review=False, mark_for_commit_queue=False):
-        self.authenticate()
-        
-        log('Adding patch "%s" to bug %s' % (description, bug_id))
-        if self.dryrun:
-            log(comment_text)
-            return
-
-        self.browser.open("%sattachment.cgi?action=enter&bugid=%s" % (self.bug_server_url, bug_id))
-        self.browser.select_form(name="entryform")
-        self._fill_attachment_form(description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue, bug_id=bug_id)
-        if comment_text:
-            log(comment_text)
-            self.browser['comment'] = comment_text
-        self.browser.submit()
-
-    def prompt_for_component(self, components):
-        log("Please pick a component:")
-        i = 0
-        for name in components:
-            i += 1
-            log("%2d. %s" % (i, name))
-        result = int(raw_input("Enter a number: ")) - 1
-        return components[result]
-
-    def _check_create_bug_response(self, response_html):
-        match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>", response_html)
-        if match:
-            return match.group('bug_id')
-
-        match = re.search('<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">', response_html, re.DOTALL)
-        error_message = "FAIL"
-        if match:
-            text_lines = BeautifulSoup(match.group('error_message')).findAll(text=True)
-            error_message = "\n" + '\n'.join(["  " + line.strip() for line in text_lines if line.strip()])
-        raise Exception("Bug not created: %s" % error_message)
-
-    def create_bug(self, bug_title, bug_description, component=None, patch_file_object=None, patch_description=None, cc=None, mark_for_review=False, mark_for_commit_queue=False):
-        self.authenticate()
-
-        log('Creating bug with title "%s"' % bug_title)
-        if self.dryrun:
-            log(bug_description)
-            return
-
-        self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit")
-        self.browser.select_form(name="Create")
-        component_items = self.browser.find_control('component').items
-        component_names = map(lambda item: item.name, component_items)
-        if not component or component not in component_names:
-            component = self.prompt_for_component(component_names)
-        self.browser['component'] = [component]
-        if cc:
-            self.browser['cc'] = cc
-        self.browser['short_desc'] = bug_title
-        self.browser['comment'] = bug_description
-
-        if patch_file_object:
-            self._fill_attachment_form(patch_description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue)
-
-        response = self.browser.submit()
-
-        bug_id = self._check_create_bug_response(response.read())
-        log("Bug %s created." % bug_id)
-        log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id))
-        return bug_id
-
-    def _find_select_element_for_flag(self, flag_name):
-        # FIXME: This will break if we ever re-order attachment flags
-        if flag_name == "review":
-            return self.browser.find_control(type='select', nr=0)
-        if flag_name == "commit-queue":
-            return self.browser.find_control(type='select', nr=1)
-        raise Exception("Don't know how to find flag named \"%s\"" % flag_name)
-
-    def clear_attachment_flags(self, attachment_id, additional_comment_text=None):
-        self.authenticate()
-
-        comment_text = "Clearing flags on attachment: %s" % attachment_id
-        if additional_comment_text:
-            comment_text += "\n\n%s" % additional_comment_text
-        log(comment_text)
-
-        if self.dryrun:
-            return
-
-        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
-        self.browser.select_form(nr=1)
-        self.browser.set_value(comment_text, name='comment', nr=0)
-        self._find_select_element_for_flag('review').value = ("X",)
-        self._find_select_element_for_flag('commit-queue').value = ("X",)
-        self.browser.submit()
-
-    # FIXME: We need a way to test this on a live bugzilla instance.
-    def _set_flag_on_attachment(self, attachment_id, flag_name, flag_value, comment_text, additional_comment_text):
-        self.authenticate()
-
-        if additional_comment_text:
-            comment_text += "\n\n%s" % additional_comment_text
-        log(comment_text)
-
-        if self.dryrun:
-            return
-
-        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
-        self.browser.select_form(nr=1)
-        self.browser.set_value(comment_text, name='comment', nr=0)
-        self._find_select_element_for_flag(flag_name).value = (flag_value,)
-        self.browser.submit()
-
-    def reject_patch_from_commit_queue(self, attachment_id, additional_comment_text=None):
-        comment_text = "Rejecting patch %s from commit-queue." % attachment_id
-        self._set_flag_on_attachment(attachment_id, 'commit-queue', '-', comment_text, additional_comment_text)
-
-    def reject_patch_from_review_queue(self, attachment_id, additional_comment_text=None):
-        comment_text = "Rejecting patch %s from review queue." % attachment_id
-        self._set_flag_on_attachment(attachment_id, 'review', '-', comment_text, additional_comment_text)
-
-    # FIXME: All of these bug editing methods have a ridiculous amount of copy/paste code.
-    def obsolete_attachment(self, attachment_id, comment_text = None):
-        self.authenticate()
-
-        log("Obsoleting attachment: %s" % attachment_id)
-        if self.dryrun:
-            log(comment_text)
-            return
-
-        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
-        self.browser.select_form(nr=1)
-        self.browser.find_control('isobsolete').items[0].selected = True
-        # Also clear any review flag (to remove it from review/commit queues)
-        self._find_select_element_for_flag('review').value = ("X",)
-        self._find_select_element_for_flag('commit-queue').value = ("X",)
-        if comment_text:
-            log(comment_text)
-            # Bugzilla has two textareas named 'comment', one is somehow hidden.  We want the first.
-            self.browser.set_value(comment_text, name='comment', nr=0)
-        self.browser.submit()
-
-    def add_cc_to_bug(self, bug_id, email_address_list):
-        self.authenticate()
-
-        log("Adding %s to the CC list for bug %s" % (email_address_list, bug_id))
-        if self.dryrun:
-            return
-
-        self.browser.open(self.bug_url_for_bug_id(bug_id))
-        self.browser.select_form(name="changeform")
-        self.browser["newcc"] = ", ".join(email_address_list)
-        self.browser.submit()
-
-    def post_comment_to_bug(self, bug_id, comment_text, cc=None):
-        self.authenticate()
-
-        log("Adding comment to bug %s" % bug_id)
-        if self.dryrun:
-            log(comment_text)
-            return
-
-        self.browser.open(self.bug_url_for_bug_id(bug_id))
-        self.browser.select_form(name="changeform")
-        self.browser["comment"] = comment_text
-        if cc:
-            self.browser["newcc"] = ", ".join(cc)
-        self.browser.submit()
-
-    def close_bug_as_fixed(self, bug_id, comment_text=None):
-        self.authenticate()
-
-        log("Closing bug %s as fixed" % bug_id)
-        if self.dryrun:
-            log(comment_text)
-            return
-
-        self.browser.open(self.bug_url_for_bug_id(bug_id))
-        self.browser.select_form(name="changeform")
-        if comment_text:
-            log(comment_text)
-            self.browser['comment'] = comment_text
-        self.browser['bug_status'] = ['RESOLVED']
-        self.browser['resolution'] = ['FIXED']
-        self.browser.submit()
-
-    def reassign_bug(self, bug_id, assignee, comment_text=None):
-        self.authenticate()
-
-        log("Assigning bug %s to %s" % (bug_id, assignee))
-        if self.dryrun:
-            log(comment_text)
-            return
-
-        self.browser.open(self.bug_url_for_bug_id(bug_id))
-        self.browser.select_form(name="changeform")
-        if comment_text:
-            log(comment_text)
-            self.browser["comment"] = comment_text
-        self.browser["assigned_to"] = assignee
-        self.browser.submit()
-
-    def reopen_bug(self, bug_id, comment_text):
-        self.authenticate()
-
-        log("Re-opening bug %s" % bug_id)
-        log(comment_text) # Bugzilla requires a comment when re-opening a bug, so we know it will never be None.
-        if self.dryrun:
-            return
-
-        self.browser.open(self.bug_url_for_bug_id(bug_id))
-        self.browser.select_form(name="changeform")
-        self.browser['bug_status'] = ['REOPENED']
-        self.browser['comment'] = comment_text
-        self.browser.submit()
diff --git a/WebKitTools/Scripts/modules/bugzilla_unittest.py b/WebKitTools/Scripts/modules/bugzilla_unittest.py
deleted file mode 100644
index bcc4b81..0000000
--- a/WebKitTools/Scripts/modules/bugzilla_unittest.py
+++ /dev/null
@@ -1,302 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.committers import CommitterList, Reviewer, Committer
-from modules.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id
-from modules.outputcapture import OutputCapture
-from modules.mock import Mock
-
-from modules.BeautifulSoup import BeautifulSoup
-
-
-class MockBrowser(object):
-    def open(self, url):
-        pass
-
-    def select_form(self, name):
-        pass
-
-    def __setitem__(self, key, value):
-        pass
-
-    def submit(self):
-        pass
-
-
-class BugzillaTest(unittest.TestCase):
-    _example_attachment = '''
-        <attachment
-          isobsolete="1"
-          ispatch="1"
-          isprivate="0"
-        >
-        <attachid>33721</attachid>
-        <date>2009-07-29 10:23 PDT</date>
-        <desc>Fixed whitespace issue</desc>
-        <filename>patch</filename>
-        <type>text/plain</type>
-        <size>9719</size>
-        <attacher>christian.plesner.hansen at gmail.com</attacher>
-          <flag name="review"
-                id="17931"
-                status="+"
-                setter="one at test.com"
-           />
-          <flag name="commit-queue"
-                id="17932"
-                status="+"
-                setter="two at test.com"
-           />
-        </attachment>
-'''
-    _expected_example_attachment_parsing = {
-        'bug_id' : 100,
-        'is_obsolete' : True,
-        'is_patch' : True,
-        'id' : 33721,
-        'url' : "https://bugs.webkit.org/attachment.cgi?id=33721",
-        'name' : "Fixed whitespace issue",
-        'type' : "text/plain",
-        'review' : '+',
-        'reviewer_email' : 'one at test.com',
-        'commit-queue' : '+',
-        'committer_email' : 'two at test.com',
-        'attacher_email' : 'christian.plesner.hansen at gmail.com',
-    }
-
-    def test_parse_bug_id(self):
-        # FIXME: These would be all better as doctests
-        bugs = Bugzilla()
-        self.assertEquals(12345, parse_bug_id("http://webkit.org/b/12345"))
-        self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345"))
-        self.assertEquals(12345, parse_bug_id(bugs.short_bug_url_for_bug_id(12345)))
-        self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345)))
-        self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345, xml=True)))
-
-        # Our bug parser is super-fragile, but at least we're testing it.
-        self.assertEquals(None, parse_bug_id("http://www.webkit.org/b/12345"))
-        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&id=12345"))
-
-    _example_bug = """
-<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
-<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
-<bugzilla version="3.2.3"
-          urlbase="https://bugs.webkit.org/"
-          maintainer="admin at webkit.org"
-          exporter="eric at webkit.org"
->
-    <bug>
-          <bug_id>32585</bug_id>
-          <creation_ts>2009-12-15 15:17 PST</creation_ts>
-          <short_desc>bug to test bugzilla-tool and commit-queue failures</short_desc>
-          <delta_ts>2009-12-27 21:04:50 PST</delta_ts>
-          <reporter_accessible>1</reporter_accessible>
-          <cclist_accessible>1</cclist_accessible>
-          <classification_id>1</classification_id>
-          <classification>Unclassified</classification>
-          <product>WebKit</product>
-          <component>Tools / Tests</component>
-          <version>528+ (Nightly build)</version>
-          <rep_platform>PC</rep_platform>
-          <op_sys>Mac OS X 10.5</op_sys>
-          <bug_status>NEW</bug_status>
-          <priority>P2</priority>
-          <bug_severity>Normal</bug_severity>
-          <target_milestone>---</target_milestone>
-          <everconfirmed>1</everconfirmed>
-          <reporter name="Eric Seidel">eric at webkit.org</reporter>
-          <assigned_to name="Nobody">webkit-unassigned at lists.webkit.org</assigned_to>
-          <cc>foo at bar.com</cc>
-    <cc>example at example.com</cc>
-          <long_desc isprivate="0">
-            <who name="Eric Seidel">eric at webkit.org</who>
-            <bug_when>2009-12-15 15:17:28 PST</bug_when>
-            <thetext>bug to test bugzilla-tool and commit-queue failures
-
-Ignore this bug.  Just for testing failure modes of bugzilla-tool and the commit-queue.</thetext>
-          </long_desc>
-          <attachment 
-              isobsolete="0"
-              ispatch="1"
-              isprivate="0"
-          > 
-            <attachid>45548</attachid> 
-            <date>2009-12-27 23:51 PST</date> 
-            <desc>Patch</desc> 
-            <filename>bug-32585-20091228005112.patch</filename> 
-            <type>text/plain</type> 
-            <size>10882</size> 
-            <attacher>mjs at apple.com</attacher> 
-            
-              <token>1261988248-dc51409e9c421a4358f365fa8bec8357</token> 
-              <data encoding="base64">SW5kZXg6IFdlYktpdC9tYWMvQ2hhbmdlTG9nCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09
-removed-because-it-was-really-long
-ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
-</data>        
- 
-              <flag name="review"
-                    id="27602"
-                    status="?"
-                    setter="mjs at apple.com"
-               /> 
-          </attachment> 
-    </bug>
-</bugzilla>
-"""
-    _expected_example_bug_parsing = {
-        "id" : 32585,
-        "title" : u"bug to test bugzilla-tool and commit-queue failures",
-        "cc_emails" : ["foo at bar.com", "example at example.com"],
-        "reporter_email" : "eric at webkit.org",
-        "assigned_to_email" : "webkit-unassigned at lists.webkit.org",
-        "attachments" : [{
-            'name': u'Patch',
-            'url': 'https://bugs.webkit.org/attachment.cgi?id=45548',
-            'is_obsolete': False,
-            'review': '?',
-            'is_patch': True,
-            'attacher_email': 'mjs at apple.com',
-            'bug_id': 32585,
-            'type': 'text/plain',
-            'id': 45548
-        }],
-    }
-
-    def _assert_dictionaries_equal(self, actual, expected):
-        # Make sure we aren't parsing more or less than we expect
-        self.assertEquals(sorted(actual.keys()), sorted(expected.keys()))
-
-        for key, expected_value in expected.items():
-            self.assertEquals(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))
-
-    def test_bug_parsing(self):
-        bug = Bugzilla()._parse_bug_page(self._example_bug)
-        self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
-
-    # This could be combined into test_bug_parsing later if desired.
-    def test_attachment_parsing(self):
-        bugzilla = Bugzilla()
-        soup = BeautifulSoup(self._example_attachment)
-        attachment_element = soup.find("attachment")
-        attachment = bugzilla._parse_attachment_element(attachment_element, self._expected_example_attachment_parsing['bug_id'])
-        self.assertTrue(attachment)
-        self._assert_dictionaries_equal(attachment, self._expected_example_attachment_parsing)
-
-    _sample_attachment_detail_page = """
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
-                      "http://www.w3.org/TR/html4/loose.dtd">
-<html>
-  <head>
-    <title>
-  Attachment 41073 Details for Bug 27314</title>
-<link rel="Top" href="https://bugs.webkit.org/">
-    <link rel="Up" href="show_bug.cgi?id=27314">
-"""
-
-    def test_attachment_detail_bug_parsing(self):
-        bugzilla = Bugzilla()
-        self.assertEquals(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
-
-    def test_add_cc_to_bug(self):
-        bugzilla = Bugzilla()
-        bugzilla.browser = MockBrowser()
-        bugzilla.authenticate = lambda: None
-        expected_stderr = "Adding ['adam at example.com'] to the CC list for bug 42\n"
-        OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam at example.com"]], expected_stderr=expected_stderr)
-
-    def test_flag_permission_rejection_message(self):
-        bugzilla = Bugzilla()
-        expected_messsage="""foo at foo.com does not have review permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/modules/committers.py.
-
-- If you do not have review rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
-
-- If you have review rights please correct the error in WebKitTools/Scripts/modules/committers.py by adding yourself to the file (no review needed).  Due to bug 30084 the commit-queue will require a restart after your change.  Please contact eseidel at chromium.org to request a commit-queue restart.  After restart the commit-queue will correctly respect your review rights."""
-        self.assertEqual(bugzilla._flag_permission_rejection_message("foo at foo.com", "review"), expected_messsage)
-
-
-class BugzillaQueriesTest(unittest.TestCase):
-    _sample_request_page = """
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
-                      "http://www.w3.org/TR/html4/loose.dtd">
-<html>
-  <head>
-    <title>Request Queue</title>
-  </head>
-<body>
-
-<h3>Flag: review</h3>
-  <table class="requests" cellspacing="0" cellpadding="4" border="1">
-    <tr>
-        <th>Requester</th>
-        <th>Requestee</th>
-        <th>Bug</th>
-        <th>Attachment</th>
-        <th>Created</th>
-    </tr>
-    <tr>
-        <td>Shinichiro Hamaji &lt;hamaji&#64;chromium.org&gt;</td>
-        <td></td>
-        <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
-        <td><a href="attachment.cgi?id=40511&amp;action=review">
-40511: Patch v0</a></td>
-        <td>2009-10-02 04:58 PST</td>
-    </tr>
-    <tr>
-        <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
-        <td></td>
-        <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
-        <td><a href="attachment.cgi?id=40722&amp;action=review">
-40722: Media controls, the simple approach</a></td>
-        <td>2009-10-06 09:13 PST</td>
-    </tr>
-    <tr>
-        <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
-        <td></td>
-        <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
-        <td><a href="attachment.cgi?id=40723&amp;action=review">
-40723: Adjust the media slider thumb size</a></td>
-        <td>2009-10-06 09:15 PST</td>
-    </tr>
-  </table>
-</body>
-</html>
-"""
-
-    def test_request_page_parsing(self):
-        queries = BugzillaQueries(None)
-        self.assertEquals([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
-
-    def test_load_query(self):
-        queries = BugzillaQueries(Mock())
-        queries._load_query("request.cgi?action=queue&type=review&group=type")
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/buildbot.py b/WebKitTools/Scripts/modules/buildbot.py
deleted file mode 100644
index df5d8c2..0000000
--- a/WebKitTools/Scripts/modules/buildbot.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# WebKit's Python module for interacting with WebKit's buildbot
-
-import re
-import urllib2
-
-# Import WebKit-specific modules.
-from modules.webkit_logging import log
-
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
-# so this import should always succeed.
-from .BeautifulSoup import BeautifulSoup
-
-class BuildBot:
-    default_host = "build.webkit.org"
-    def __init__(self, host=default_host):
-        self.buildbot_host = host
-        self.buildbot_server_url = "http://%s/" % self.buildbot_host
-        
-        # If any of the Leopard build/test bots or the Windows builders are red we should not be landing patches.
-        # Other builders should be added to this list once they're known to be stable.
-        self.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
-
-    # If WebKit's buildbot has an XMLRPC interface we could use, we could do something more sophisticated here.
-    # For now we just parse out the basics, enough to support basic questions like "is the tree green?"
-    def _parse_builder_status_from_row(self, status_row):
-        status_cells = status_row.findAll('td')
-        builder = {}
-
-        name_link = status_cells[0].find('a')
-        builder['name'] = name_link.string
-        # We could generate the builder_url from the name in a future version of this code.
-        builder['builder_url'] = self.buildbot_server_url + name_link['href']
-
-        status_link = status_cells[1].find('a')
-        if not status_link:
-            # We failed to find a link in the first cell, just give up.
-            # This can happen if a builder is just-added, the first cell will just be "no build"
-            builder['is_green'] = False # Other parts of the code depend on is_green being present.
-            return builder
-        revision_string = status_link.string # Will be either a revision number or a build number
-        # If revision_string has non-digits assume it's not a revision number.
-        builder['built_revision'] = int(revision_string) if not re.match('\D', revision_string) else None
-        builder['is_green'] = not re.search('fail', status_cells[1].renderContents())
-        # We could parse out the build number instead, but for now just store the URL.
-        builder['build_url'] = self.buildbot_server_url + status_link['href']
-
-        # We could parse out the current activity too.
-
-        return builder
-
-    def _builder_statuses_with_names_matching_regexps(self, builder_statuses, name_regexps):
-        builders = []
-        for builder in builder_statuses:
-            for name_regexp in name_regexps:
-                if re.match(name_regexp, builder['name']):
-                    builders.append(builder)
-        return builders
-
-    def red_core_builders(self):
-        red_builders = []
-        for builder in self._builder_statuses_with_names_matching_regexps(self.builder_statuses(), self.core_builder_names_regexps):
-            if not builder['is_green']:
-                red_builders.append(builder)
-        return red_builders
-
-    def red_core_builders_names(self):
-        red_builders = self.red_core_builders()
-        return map(lambda builder: builder['name'], red_builders)
-
-    def core_builders_are_green(self):
-        return not self.red_core_builders()
-
-    def builder_statuses(self):
-        build_status_url = self.buildbot_server_url + 'one_box_per_builder'
-        page = urllib2.urlopen(build_status_url)
-        soup = BeautifulSoup(page)
-
-        builders = []
-        status_table = soup.find('table')
-        for status_row in status_table.findAll('tr'):
-            builder = self._parse_builder_status_from_row(status_row)
-            builders.append(builder)
-        return builders
diff --git a/WebKitTools/Scripts/modules/buildbot_unittest.py b/WebKitTools/Scripts/modules/buildbot_unittest.py
deleted file mode 100644
index a85f2ea..0000000
--- a/WebKitTools/Scripts/modules/buildbot_unittest.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.buildbot import BuildBot
-
-from modules.BeautifulSoup import BeautifulSoup
-
-class BuildBotTest(unittest.TestCase):
-
-    _example_one_box_status = '''
-    <table>
-    <tr>
-    <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td>
-      <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td>
-      <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td>
-    <tr>
-    <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td>
-      <td class="LastBuild box" >no build</td>
-      <td align="center" class="Activity building">building<br />< 1 min</td>
-    <tr>
-    <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td>
-      <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td>
-      <td align="center" class="Activity idle">idle</td>
-    </table>
-'''
-    _expected_example_one_box_parsings = [
-        {
-            'builder_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29',
-            'build_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29/builds/3693',
-            'is_green': True,
-            'name': u'Windows Debug (Tests)',
-            'built_revision': 47380
-        },
-        {
-            'builder_url': u'http://build.webkit.org/builders/SnowLeopard%20Intel%20Release',
-            'is_green': False,
-            'name': u'SnowLeopard Intel Release',
-        },
-        {
-            'builder_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release',
-            'build_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release/builds/654',
-            'is_green': False,
-            'name': u'Qt Linux Release',
-            'built_revision': 47383
-        },
-    ]
-
-    def test_status_parsing(self):
-        buildbot = BuildBot()
-
-        soup = BeautifulSoup(self._example_one_box_status)
-        status_table = soup.find("table")
-        input_rows = status_table.findAll('tr')
-
-        for x in range(len(input_rows)):
-            status_row = input_rows[x]
-            expected_parsing = self._expected_example_one_box_parsings[x]
-
-            builder = buildbot._parse_builder_status_from_row(status_row)
-
-            # Make sure we aren't parsing more or less than we expect
-            self.assertEquals(builder.keys(), expected_parsing.keys())
-
-            for key, expected_value in expected_parsing.items():
-                self.assertEquals(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value)))
-
-    def test_core_builder_methods(self):
-        buildbot = BuildBot()
-
-        # Override builder_statuses function to not touch the network.
-        def example_builder_statuses(): # We could use instancemethod() to bind 'self' but we don't need to.
-            return BuildBotTest._expected_example_one_box_parsings
-        buildbot.builder_statuses = example_builder_statuses
-
-        buildbot.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
-        self.assertEquals(buildbot.red_core_builders_names(), [])
-        self.assertTrue(buildbot.core_builders_are_green())
-
-        buildbot.core_builder_names_regexps = [ 'SnowLeopard', 'Qt' ]
-        self.assertEquals(buildbot.red_core_builders_names(), [ u'SnowLeopard Intel Release', u'Qt Linux Release' ])
-        self.assertFalse(buildbot.core_builders_are_green())
-
-    def test_builder_name_regexps(self):
-        buildbot = BuildBot()
-
-        example_builders = [
-            { 'name': u'Leopard Debug (Build)', },
-            { 'name': u'Leopard Debug (Tests)', },
-            { 'name': u'Windows Release (Build)', },
-            { 'name': u'Windows Debug (Tests)', },
-            { 'name': u'Qt Linux Release', },
-        ]
-        name_regexps = [ 'Leopard', "Windows.*Build" ]
-        expected_builders = [
-            { 'name': u'Leopard Debug (Build)', },
-            { 'name': u'Leopard Debug (Tests)', },
-            { 'name': u'Windows Release (Build)', },
-        ]
-
-        # This test should probably be updated if the default regexp list changes
-        self.assertEquals(buildbot.core_builder_names_regexps, name_regexps)
-
-        builders = buildbot._builder_statuses_with_names_matching_regexps(example_builders, name_regexps)
-        self.assertEquals(builders, expected_builders)
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/buildsteps.py b/WebKitTools/Scripts/modules/buildsteps.py
deleted file mode 100644
index 1b12e32..0000000
--- a/WebKitTools/Scripts/modules/buildsteps.py
+++ /dev/null
@@ -1,541 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import StringIO
-
-from optparse import make_option
-
-from modules.comments import bug_comment_from_commit_text
-from modules.grammar import pluralize
-from modules.webkit_logging import log, error
-from modules.webkitport import WebKitPort
-from modules.changelogs import ChangeLog
-
-# FIXME: Why do some of these have "Step" in their name but not all?
-__all__ = [
-    "ApplyPatchStep",
-    "ApplyPatchWithLocalCommitStep",
-    "BuildStep",
-    "CheckStyleStep",
-    "CleanWorkingDirectoryStep",
-    "CleanWorkingDirectoryWithLocalCommitsStep",
-    "CloseBugForLandDiffStep",
-    "CloseBugStep",
-    "ClosePatchStep",
-    "CommitStep",
-    "CompleteRollout",
-    "CreateBugStep",
-    "EnsureBuildersAreGreenStep",
-    "EnsureLocalCommitIfNeeded",
-    "ObsoletePatchesOnBugStep",
-    "PostDiffToBugStep",
-    "PrepareChangeLogForRevertStep",
-    "PrepareChangeLogStep",
-    "PromptForBugOrTitleStep",
-    "RevertRevisionStep",
-    "RunTestsStep",
-    "UpdateChangeLogsWithReviewerStep",
-    "UpdateStep",
-]
-
-class CommandOptions(object):
-    force_clean = make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)")
-    clean = make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches")
-    check_builders = make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="Don't check to see if the build.webkit.org builders are green before landing.")
-    quiet = make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output.")
-    non_interactive = make_option("--non-interactive", action="store_true", dest="non_interactive", default=False, help="Never prompt the user, fail as fast as possible.")
-    parent_command = make_option("--parent-command", action="store", dest="parent_command", default=None, help="(Internal) The command that spawned this instance.")
-    update = make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory.")
-    local_commit = make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch")
-    build = make_option("--no-build", action="store_false", dest="build", default=True, help="Commit without building first, implies --no-test.")
-    build_style = make_option("--build-style", action="store", dest="build_style", default=None, help="Whether to build debug, release, or both.")
-    test = make_option("--no-test", action="store_false", dest="test", default=True, help="Commit without running run-webkit-tests.")
-    close_bug = make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing.")
-    port = make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...).")
-    reviewer = make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER.")
-    complete_rollout = make_option("--complete-rollout", action="store_true", dest="complete_rollout", help="Commit the revert and re-open the original bug.")
-    obsolete_patches = make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one.")
-    review = make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review.")
-    request_commit = make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review.")
-    description = make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: \"patch\")")
-    cc = make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy.")
-    component = make_option("--component", action="store", type="string", dest="component", help="Component for the new bug.")
-    confirm = make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Skip confirmation steps.")
-
-
-class AbstractStep(object):
-    def __init__(self, tool, options):
-        self._tool = tool
-        self._options = options
-        self._port = None
-
-    def _run_script(self, script_name, quiet=False, port=WebKitPort):
-        log("Running %s" % script_name)
-        # FIXME: This should use self.port()
-        self._tool.executive.run_and_throw_if_fail(port.script_path(script_name), quiet)
-
-    # FIXME: The port should live on the tool.
-    def port(self):
-        if self._port:
-            return self._port
-        self._port = WebKitPort.port(self._options.port)
-        return self._port
-
-    @classmethod
-    def options(cls):
-        return []
-
-    def run(self, state):
-        raise NotImplementedError, "subclasses must implement"
-
-
-# FIXME: Unify with StepSequence?  I'm not sure yet which is the better design.
-class MetaStep(AbstractStep):
-    substeps = [] # Override in subclasses
-    def __init__(self, tool, options):
-        AbstractStep.__init__(self, tool, options)
-        self._step_instances = []
-        for step_class in self.substeps:
-            self._step_instances.append(step_class(tool, options))
-
-    @staticmethod
-    def _collect_options_from_steps(steps):
-        collected_options = []
-        for step in steps:
-            collected_options = collected_options + step.options()
-        return collected_options
-
-    @classmethod
-    def options(cls):
-        return cls._collect_options_from_steps(cls.substeps)
-
-    def run(self, state):
-        for step in self._step_instances:
-             step.run(state)
-
-
-class PromptForBugOrTitleStep(AbstractStep):
-    def run(self, state):
-        # No need to prompt if we alrady have the bug_id.
-        if state.get("bug_id"):
-            return
-        user_response = self._tool.user.prompt("Please enter a bug number or a title for a new bug:\n")
-        # If the user responds with a number, we assume it's bug number.
-        # Otherwise we assume it's a bug subject.
-        try:
-            state["bug_id"] = int(user_response)
-        except ValueError, TypeError:
-            state["bug_title"] = user_response
-            # FIXME: This is kind of a lame description.
-            state["bug_description"] = user_response
-
-
-class CreateBugStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.cc,
-            CommandOptions.component,
-        ]
-
-    def run(self, state):
-        # No need to create a bug if we already have one.
-        if state.get("bug_id"):
-            return
-        state["bug_id"] = self._tool.bugs.create_bug(state["bug_title"], state["bug_description"], component=self._options.component, cc=self._options.cc)
-
-
-class PrepareChangeLogStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.port,
-            CommandOptions.quiet,
-        ]
-
-    def run(self, state):
-        os.chdir(self._tool.scm().checkout_root)
-        args = [self.port().script_path("prepare-ChangeLog")]
-        if state["bug_id"]:
-            args.append("--bug=%s" % state["bug_id"])
-        self._tool.executive.run_and_throw_if_fail(args, self._options.quiet)
-
-
-class EditChangeLogStep(AbstractStep):
-    def run(self, state):
-        self._tool.user.edit(self._tool.scm().modified_changelogs())
-
-
-class ObsoletePatchesOnBugStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.obsolete_patches,
-        ]
-
-    def run(self, state):
-        if not self._options.obsolete_patches:
-            return
-        bug_id = state["bug_id"]
-        patches = self._tool.bugs.fetch_patches_from_bug(bug_id)
-        if not patches:
-            return
-        log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
-        for patch in patches:
-            self._tool.bugs.obsolete_attachment(patch["id"])
-
-
-class AbstractDiffStep(AbstractStep):
-    def diff(self, state):
-        diff = state.get("diff")
-        if not diff:
-            diff = self._tool.scm().create_patch()
-            state["diff"] = diff
-        return diff
-
-
-class ConfirmDiffStep(AbstractDiffStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.confirm,
-        ]
-
-    def run(self, state):
-        if not self._options.confirm:
-            return
-        diff = self.diff(state)
-        self._tool.user.page(diff)
-        if not self._tool.user.confirm():
-            error("User declined to continue.")
-
-
-class PostDiffToBugStep(AbstractDiffStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.description,
-            CommandOptions.review,
-            CommandOptions.request_commit,
-        ]
-
-    def run(self, state):
-        diff = self.diff(state)
-        diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
-        description = self._options.description or "Patch"
-        self._tool.bugs.add_patch_to_bug(state["bug_id"], diff_file, description, mark_for_review=self._options.review, mark_for_commit_queue=self._options.request_commit)
-
-
-class PrepareChangeLogForRevertStep(AbstractStep):
-    def run(self, state):
-        # First, discard the ChangeLog changes from the rollout.
-        os.chdir(self._tool.scm().checkout_root)
-        changelog_paths = self._tool.scm().modified_changelogs()
-        self._tool.scm().revert_files(changelog_paths)
-
-        # Second, make new ChangeLog entries for this rollout.
-        # This could move to prepare-ChangeLog by adding a --revert= option.
-        self._run_script("prepare-ChangeLog")
-        for changelog_path in changelog_paths:
-            ChangeLog(changelog_path).update_for_revert(state["revision"])
-
-
-class CleanWorkingDirectoryStep(AbstractStep):
-    def __init__(self, tool, options, allow_local_commits=False):
-        AbstractStep.__init__(self, tool, options)
-        self._allow_local_commits = allow_local_commits
-
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.force_clean,
-            CommandOptions.clean,
-        ]
-
-    def run(self, state):
-        os.chdir(self._tool.scm().checkout_root)
-        if not self._allow_local_commits:
-            self._tool.scm().ensure_no_local_commits(self._options.force_clean)
-        if self._options.clean:
-            self._tool.scm().ensure_clean_working_directory(force_clean=self._options.force_clean)
-
-
-class CleanWorkingDirectoryWithLocalCommitsStep(CleanWorkingDirectoryStep):
-    def __init__(self, tool, options):
-        # FIXME: This a bit of a hack.  Consider doing this more cleanly.
-        CleanWorkingDirectoryStep.__init__(self, tool, options, allow_local_commits=True)
-
-
-class UpdateStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.update,
-            CommandOptions.port,
-        ]
-
-    def run(self, state):
-        if not self._options.update:
-            return
-        log("Updating working directory")
-        self._tool.executive.run_and_throw_if_fail(self.port().update_webkit_command(), quiet=True)
-
-
-class ApplyPatchStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.non_interactive,
-        ]
-
-    def run(self, state):
-        log("Processing patch %s from bug %s." % (state["patch"]["id"], state["patch"]["bug_id"]))
-        self._tool.scm().apply_patch(state["patch"], force=self._options.non_interactive)
-
-
-class RevertRevisionStep(AbstractStep):
-    def run(self, state):
-        self._tool.scm().apply_reverse_diff(state["revision"])
-
-
-class ApplyPatchWithLocalCommitStep(ApplyPatchStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.local_commit,
-        ] + ApplyPatchStep.options()
-    
-    def run(self, state):
-        ApplyPatchStep.run(self, state)
-        if self._options.local_commit:
-            commit_message = self._tool.scm().commit_message_for_this_commit()
-            self._tool.scm().commit_locally_with_message(commit_message.message() or state["patch"]["name"])
-
-
-class EnsureBuildersAreGreenStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.check_builders,
-        ]
-
-    def run(self, state):
-        if not self._options.check_builders:
-            return
-        red_builders_names = self._tool.buildbot.red_core_builders_names()
-        if not red_builders_names:
-            return
-        red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
-        error("Builders [%s] are red, please do not commit.\nSee http://%s.\nPass --ignore-builders to bypass this check." % (", ".join(red_builders_names), self._tool.buildbot.buildbot_host))
-
-
-class EnsureLocalCommitIfNeeded(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.local_commit,
-        ]
-
-    def run(self, state):
-        if self._options.local_commit and not self._tool.scm().supports_local_commits():
-            error("--local-commit passed, but %s does not support local commits" % self._tool.scm.display_name())
-
-
-class UpdateChangeLogsWithReviewerStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.reviewer,
-        ]
-
-    def _guess_reviewer_from_bug(self, bug_id):
-        patches = self._tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
-        if len(patches) != 1:
-            log("%s on bug %s, cannot infer reviewer." % (pluralize("reviewed patch", len(patches)), bug_id))
-            return None
-        patch = patches[0]
-        reviewer = patch["reviewer"]
-        log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (reviewer, patch["id"], bug_id))
-        return reviewer
-
-    def run(self, state):
-        bug_id = state["patch"]["bug_id"]
-        reviewer = self._options.reviewer
-        if not reviewer:
-            if not bug_id:
-                log("No bug id provided and --reviewer= not provided.  Not updating ChangeLogs with reviewer.")
-                return
-            reviewer = self._guess_reviewer_from_bug(bug_id)
-
-        if not reviewer:
-            log("Failed to guess reviewer from bug %s and --reviewer= not provided.  Not updating ChangeLogs with reviewer." % bug_id)
-            return
-
-        os.chdir(self._tool.scm().checkout_root)
-        for changelog_path in self._tool.scm().modified_changelogs():
-            ChangeLog(changelog_path).set_reviewer(reviewer)
-
-
-class BuildStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.build,
-            CommandOptions.quiet,
-            CommandOptions.build_style,
-        ]
-
-    def build(self, build_style):
-        self._tool.executive.run_and_throw_if_fail(self.port().build_webkit_command(build_style=build_style), self._options.quiet)
-
-    def run(self, state):
-        if not self._options.build:
-            return
-        log("Building WebKit")
-        if self._options.build_style == "both":
-            self.build("debug")
-            self.build("release")
-        else:
-            self.build(self._options.build_style)
-
-
-class CheckStyleStep(AbstractStep):
-    def run(self, state):
-        self._run_script("check-webkit-style")
-
-
-class RunTestsStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.build,
-            CommandOptions.test,
-            CommandOptions.non_interactive,
-            CommandOptions.quiet,
-            CommandOptions.port,
-        ]
-
-    def run(self, state):
-        if not self._options.build:
-            return
-        if not self._options.test:
-            return
-        args = self.port().run_webkit_tests_command()
-        if self._options.non_interactive:
-            args.append("--no-launch-safari")
-            args.append("--exit-after-n-failures=1")
-        if self._options.quiet:
-            args.append("--quiet")
-        self._tool.executive.run_and_throw_if_fail(args)
-
-
-class CommitStep(AbstractStep):
-    def run(self, state):
-        commit_message = self._tool.scm().commit_message_for_this_commit()
-        state["commit_text"] =  self._tool.scm().commit_with_message(commit_message.message())
-
-
-class ClosePatchStep(AbstractStep):
-    def run(self, state):
-        comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
-        self._tool.bugs.clear_attachment_flags(state["patch"]["id"], comment_text)
-
-
-class CloseBugStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.close_bug,
-        ]
-
-    def run(self, state):
-        if not self._options.close_bug:
-            return
-        # Check to make sure there are no r? or r+ patches on the bug before closing.
-        # Assume that r- patches are just previous patches someone forgot to obsolete.
-        patches = self._tool.bugs.fetch_patches_from_bug(state["patch"]["bug_id"])
-        for patch in patches:
-            review_flag = patch.get("review")
-            if review_flag == "?" or review_flag == "+":
-                log("Not closing bug %s as attachment %s has review=%s.  Assuming there are more patches to land from this bug." % (patch["bug_id"], patch["id"], review_flag))
-                return
-        self._tool.bugs.close_bug_as_fixed(state["patch"]["bug_id"], "All reviewed patches have been landed.  Closing bug.")
-
-
-class CloseBugForLandDiffStep(AbstractStep):
-    @classmethod
-    def options(cls):
-        return [
-            CommandOptions.close_bug,
-        ]
-
-    def run(self, state):
-        comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
-        bug_id = state["patch"]["bug_id"]
-        if bug_id:
-            log("Updating bug %s" % bug_id)
-            if self._options.close_bug:
-                self._tool.bugs.close_bug_as_fixed(bug_id, comment_text)
-            else:
-                # FIXME: We should a smart way to figure out if the patch is attached
-                # to the bug, and if so obsolete it.
-                self._tool.bugs.post_comment_to_bug(bug_id, comment_text)
-        else:
-            log(comment_text)
-            log("No bug id provided.")
-
-
-class CompleteRollout(MetaStep):
-    substeps = [
-        BuildStep,
-        CommitStep,
-    ]
-
-    @classmethod
-    def options(cls):
-        collected_options = cls._collect_options_from_steps(cls.substeps)
-        collected_options.append(CommandOptions.complete_rollout)
-        return collected_options
-
-    def run(self, state):
-        bug_id = state["bug_id"]
-        # FIXME: Fully automated rollout is not 100% idiot-proof yet, so for now just log with instructions on how to complete the rollout.
-        # Once we trust rollout we will remove this option.
-        if not self._options.complete_rollout:
-            log("\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff %s\" to commit the rollout." % bug_id)
-            return
-
-        MetaStep.run(self, state)
-
-        if not bug_id:
-            log(state["commit_text"])
-            log("No bugs were updated or re-opened to reflect this rollout.")
-            return
-        # FIXME: I'm not sure state["commit_text"] is quite right here.
-        self._tool.bugs.reopen_bug(bug_id, state["commit_text"])
diff --git a/WebKitTools/Scripts/modules/buildsteps_unittest.py b/WebKitTools/Scripts/modules/buildsteps_unittest.py
deleted file mode 100644
index c603491..0000000
--- a/WebKitTools/Scripts/modules/buildsteps_unittest.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.buildsteps import UpdateChangeLogsWithReviewerStep, UpdateStep, PromptForBugOrTitleStep
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-from modules.mock import Mock
-
-
-class UpdateChangeLogsWithReviewerStepTest(unittest.TestCase):
-    def test_guess_reviewer_from_bug(self):
-        capture = OutputCapture()
-        step = UpdateChangeLogsWithReviewerStep(MockBugzillaTool(), [])
-        expected_stderr = "0 reviewed patches on bug 75, cannot infer reviewer.\n"
-        capture.assert_outputs(self, step._guess_reviewer_from_bug, [75], expected_stderr=expected_stderr)
-
-
-class StepsTest(unittest.TestCase):
-    def _run_step(self, step, tool=None, options=None, state=None):
-        if not tool:
-            tool = MockBugzillaTool()
-        if not options:
-            options = Mock()
-        if not state:
-            state = {}
-        step(tool, options).run(state)
-
-    def test_update_step(self):
-        options = Mock()
-        options.update = True
-        self._run_step(UpdateStep, options)
-
-    def test_prompt_for_bug_or_title_step(self):
-        tool = MockBugzillaTool()
-        tool.user.prompt = lambda message: 42
-        self._run_step(PromptForBugOrTitleStep, tool=tool)
diff --git a/WebKitTools/Scripts/modules/commands/commandtest.py b/WebKitTools/Scripts/modules/commands/commandtest.py
deleted file mode 100644
index e6ba4f7..0000000
--- a/WebKitTools/Scripts/modules/commands/commandtest.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.mock import Mock
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-
-class CommandsTest(unittest.TestCase):
-    def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=Mock(), tool=MockBugzillaTool()):
-        command.bind_to_tool(tool)
-        OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/download.py b/WebKitTools/Scripts/modules/commands/download.py
deleted file mode 100644
index 7ea956c..0000000
--- a/WebKitTools/Scripts/modules/commands/download.py
+++ /dev/null
@@ -1,287 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-
-from optparse import make_option
-
-from modules.bugzilla import parse_bug_id
-# We could instead use from modules import buildsteps and then prefix every buildstep with "buildsteps."
-from modules.buildsteps import *
-from modules.changelogs import ChangeLog
-from modules.comments import bug_comment_from_commit_text
-from modules.executive import ScriptError
-from modules.grammar import pluralize
-from modules.webkit_logging import error, log
-from modules.multicommandtool import AbstractDeclarativeCommmand, Command
-from modules.stepsequence import StepSequence
-
-
-# FIXME: Move this to a more general location.
-class AbstractSequencedCommmand(AbstractDeclarativeCommmand):
-    steps = None
-    def __init__(self):
-        self._sequence = StepSequence(self.steps)
-        AbstractDeclarativeCommmand.__init__(self, self._sequence.options())
-
-    def _prepare_state(self, options, args, tool):
-        return None
-
-    def execute(self, options, args, tool):
-        self._sequence.run_and_handle_errors(tool, options, self._prepare_state(options, args, tool))
-
-
-class Build(AbstractSequencedCommmand):
-    name = "build"
-    help_text = "Update working copy and build"
-    steps = [
-        CleanWorkingDirectoryStep,
-        UpdateStep,
-        BuildStep,
-    ]
-
-
-class BuildAndTest(AbstractSequencedCommmand):
-    name = "build-and-test"
-    help_text = "Update working copy, build, and run the tests"
-    steps = [
-        CleanWorkingDirectoryStep,
-        UpdateStep,
-        BuildStep,
-        RunTestsStep,
-    ]
-
-
-class LandDiff(AbstractSequencedCommmand):
-    name = "land-diff"
-    help_text = "Land the current working directory diff and updates the associated bug if any"
-    argument_names = "[BUGID]"
-    show_in_main_help = True
-    steps = [
-        EnsureBuildersAreGreenStep,
-        UpdateChangeLogsWithReviewerStep,
-        EnsureBuildersAreGreenStep,
-        BuildStep,
-        RunTestsStep,
-        CommitStep,
-        CloseBugForLandDiffStep,
-    ]
-
-    def _prepare_state(self, options, args, tool):
-        return {
-            "patch" : {
-                "id" : None,
-                "bug_id" : (args and args[0]) or parse_bug_id(tool.scm().create_patch()),
-            }
-        }
-
-
-class AbstractPatchProcessingCommand(AbstractDeclarativeCommmand):
-    # Subclasses must implement the methods below.  We don't declare them here
-    # because we want to be able to implement them with mix-ins.
-    #
-    # def _fetch_list_of_patches_to_process(self, options, args, tool):
-    # def _prepare_to_process(self, options, args, tool):
-
-    @staticmethod
-    def _collect_patches_by_bug(patches):
-        bugs_to_patches = {}
-        for patch in patches:
-            bug_id = patch["bug_id"]
-            bugs_to_patches[bug_id] = bugs_to_patches.get(bug_id, []) + [patch]
-        return bugs_to_patches
-
-    def execute(self, options, args, tool):
-        self._prepare_to_process(options, args, tool)
-        patches = self._fetch_list_of_patches_to_process(options, args, tool)
-
-        # It's nice to print out total statistics.
-        bugs_to_patches = self._collect_patches_by_bug(patches)
-        log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
-
-        for patch in patches:
-            self._process_patch(patch, options, args, tool)
-
-
-class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
-    prepare_steps = None
-    main_steps = None
-
-    def __init__(self):
-        options = []
-        self._prepare_sequence = StepSequence(self.prepare_steps)
-        self._main_sequence = StepSequence(self.main_steps)
-        options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
-        AbstractPatchProcessingCommand.__init__(self, options)
-
-    def _prepare_to_process(self, options, args, tool):
-        self._prepare_sequence.run_and_handle_errors(tool, options)
-
-    def _process_patch(self, patch, options, args, tool):
-        state = { "patch" : patch }
-        self._main_sequence.run_and_handle_errors(tool, options, state)
-
-
-class ProcessAttachmentsMixin(object):
-    def _fetch_list_of_patches_to_process(self, options, args, tool):
-        return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
-
-
-class ProcessBugsMixin(object):
-    def _fetch_list_of_patches_to_process(self, options, args, tool):
-        all_patches = []
-        for bug_id in args:
-            patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
-            log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
-            all_patches += patches
-        return all_patches
-
-
-class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
-    name = "check-style"
-    help_text = "Run check-webkit-style on the specified attachments"
-    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
-    main_steps = [
-        CleanWorkingDirectoryStep,
-        UpdateStep,
-        ApplyPatchStep,
-        CheckStyleStep,
-    ]
-
-
-class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
-    name = "build-attachment"
-    help_text = "Apply and build patches from bugzilla"
-    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
-    main_steps = [
-        CleanWorkingDirectoryStep,
-        UpdateStep,
-        ApplyPatchStep,
-        BuildStep,
-    ]
-
-
-class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
-    prepare_steps = [
-        EnsureLocalCommitIfNeeded,
-        CleanWorkingDirectoryWithLocalCommitsStep,
-        UpdateStep,
-    ]
-    main_steps = [
-        ApplyPatchWithLocalCommitStep,
-    ]
-
-
-class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
-    name = "apply-attachment"
-    help_text = "Apply an attachment to the local working directory"
-    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
-    show_in_main_help = True
-
-
-class ApplyPatches(AbstractPatchApplyingCommand, ProcessBugsMixin):
-    name = "apply-patches"
-    help_text = "Apply reviewed patches from provided bugs to the local working directory"
-    argument_names = "BUGID [BUGIDS]"
-    show_in_main_help = True
-
-
-class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
-    prepare_steps = [
-        EnsureBuildersAreGreenStep,
-    ]
-    main_steps = [
-        CleanWorkingDirectoryStep,
-        UpdateStep,
-        ApplyPatchStep,
-        EnsureBuildersAreGreenStep,
-        BuildStep,
-        RunTestsStep,
-        CommitStep,
-        ClosePatchStep,
-        CloseBugStep,
-    ]
-
-
-class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
-    name = "land-attachment"
-    help_text = "Land patches from bugzilla, optionally building and testing them first"
-    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
-    show_in_main_help = True
-
-
-class LandPatches(AbstractPatchLandingCommand, ProcessBugsMixin):
-    name = "land-patches"
-    help_text = "Land all patches on the given bugs, optionally building and testing them first"
-    argument_names = "BUGID [BUGIDS]"
-    show_in_main_help = True
-
-
-# FIXME: Make Rollout more declarative.
-class Rollout(Command):
-    name = "rollout"
-    show_in_main_help = True
-    def __init__(self):
-        self._sequence = StepSequence([
-            CleanWorkingDirectoryStep,
-            UpdateStep,
-            RevertRevisionStep,
-            PrepareChangeLogForRevertStep,
-            CompleteRollout,
-        ])
-        Command.__init__(self, "Revert the given revision in the working copy and optionally commit the revert and re-open the original bug", "REVISION [BUGID]", options=self._sequence.options())
-
-    @staticmethod
-    def _parse_bug_id_from_revision_diff(tool, revision):
-        original_diff = tool.scm().diff_for_revision(revision)
-        return parse_bug_id(original_diff)
-
-    @staticmethod
-    def _reopen_bug_after_rollout(tool, bug_id, comment_text):
-        if bug_id:
-            tool.bugs.reopen_bug(bug_id, comment_text)
-        else:
-            log(comment_text)
-            log("No bugs were updated or re-opened to reflect this rollout.")
-
-    def execute(self, options, args, tool):
-        revision = args[0]
-        bug_id = self._parse_bug_id_from_revision_diff(tool, revision)
-        if options.complete_rollout:
-            if bug_id:
-                log("Will re-open bug %s after rollout." % bug_id)
-            else:
-                log("Failed to parse bug number from diff.  No bugs will be updated/reopened after the rollout.")
-
-        state = {
-            "revision": revision,
-            "bug_id": bug_id,
-        }
-        self._sequence.run_and_handle_errors(tool, options, state)
diff --git a/WebKitTools/Scripts/modules/commands/download_unittest.py b/WebKitTools/Scripts/modules/commands/download_unittest.py
deleted file mode 100644
index c2b4f86..0000000
--- a/WebKitTools/Scripts/modules/commands/download_unittest.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.commands.commandtest import CommandsTest
-from modules.commands.download import *
-from modules.mock import Mock
-
-class DownloadCommandsTest(CommandsTest):
-    def _default_options(self):
-        options = Mock()
-        options.force_clean = False
-        options.clean = True
-        options.check_builders = True
-        options.quiet = False
-        options.non_interactive = False
-        options.update = True
-        options.build = True
-        options.test = True
-        options.close_bug = True
-        options.complete_rollout = False
-        return options
-
-    def test_build(self):
-        expected_stderr = "Updating working directory\nBuilding WebKit\n"
-        self.assert_execute_outputs(Build(), [], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_build_and_test(self):
-        expected_stderr = "Updating working directory\nBuilding WebKit\n"
-        self.assert_execute_outputs(BuildAndTest(), [], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_apply_attachment(self):
-        options = self._default_options()
-        options.update = True
-        options.local_commit = True
-        expected_stderr = "Updating working directory\nProcessing 1 patch from 1 bug.\nProcessing patch 197 from bug 42.\n"
-        self.assert_execute_outputs(ApplyAttachment(), [197], options=options, expected_stderr=expected_stderr)
-
-    def test_apply_patches(self):
-        options = self._default_options()
-        options.update = True
-        options.local_commit = True
-        expected_stderr = "Updating working directory\n2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nProcessing patch 197 from bug 42.\nProcessing patch 128 from bug 42.\n"
-        self.assert_execute_outputs(ApplyPatches(), [42], options=options, expected_stderr=expected_stderr)
-
-    def test_land_diff(self):
-        expected_stderr = "Building WebKit\nUpdating bug 42\n"
-        self.assert_execute_outputs(LandDiff(), [42], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_check_style(self):
-        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nRunning check-webkit-style\n"
-        self.assert_execute_outputs(CheckStyle(), [197], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_build_attachment(self):
-        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
-        self.assert_execute_outputs(BuildAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_land_attachment(self):
-        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
-        self.assert_execute_outputs(LandAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_land_patches(self):
-        expected_stderr = "2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\nUpdating working directory\nProcessing patch 128 from bug 42.\nBuilding WebKit\n"
-        self.assert_execute_outputs(LandPatches(), [42], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_rollout(self):
-        expected_stderr = "Updating working directory\nRunning prepare-ChangeLog\n\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff 12345\" to commit the rollout.\n"
-        self.assert_execute_outputs(Rollout(), [852], options=self._default_options(), expected_stderr=expected_stderr)
-
-    def test_complete_rollout(self):
-        options = self._default_options()
-        options.complete_rollout = True
-        expected_stderr = "Will re-open bug 12345 after rollout.\nUpdating working directory\nRunning prepare-ChangeLog\nBuilding WebKit\n"
-        self.assert_execute_outputs(Rollout(), [852], options=options, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/early_warning_system.py b/WebKitTools/Scripts/modules/commands/early_warning_system.py
deleted file mode 100644
index a96eaf1..0000000
--- a/WebKitTools/Scripts/modules/commands/early_warning_system.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from StringIO import StringIO
-
-from modules.commands.queues import AbstractReviewQueue
-from modules.committers import CommitterList
-from modules.executive import ScriptError
-from modules.webkitport import WebKitPort
-
-
-class AbstractEarlyWarningSystem(AbstractReviewQueue):
-    def __init__(self):
-        AbstractReviewQueue.__init__(self)
-        self.port = WebKitPort.port(self.port_name)
-
-    def should_proceed_with_work_item(self, patch):
-        try:
-            self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
-            self._update_status("Building", patch)
-        except ScriptError, e:
-            self._update_status("Unable to perform a build")
-            return False
-        return True
-
-    def process_work_item(self, patch):
-        try:
-            self.run_bugzilla_tool([
-                "build-attachment",
-                self.port.flag(),
-                "--force-clean",
-                "--quiet",
-                "--non-interactive",
-                "--parent-command=%s" % self.name,
-                "--no-update",
-                patch["id"]])
-            self._did_pass(patch)
-        except ScriptError, e:
-            self._did_fail(patch)
-            raise e
-
-    @classmethod
-    def handle_script_error(cls, tool, state, script_error):
-        status_id = cls._update_status_for_script_error(tool, state, script_error)
-        # FIXME: This won't be right for ports that don't use build-webkit!
-        if not script_error.command_name() == "build-webkit":
-            return
-        results_link = tool.status_bot.results_url_for_status(status_id)
-        message = "Attachment %s did not build on %s:\nBuild output: %s" % (state["patch"]["id"], cls.port_name, results_link)
-        tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
-
-
-class GtkEWS(AbstractEarlyWarningSystem):
-    name = "gtk-ews"
-    port_name = "gtk"
-
-
-class QtEWS(AbstractEarlyWarningSystem):
-    name = "qt-ews"
-    port_name = "qt"
-
-
-class ChromiumEWS(AbstractEarlyWarningSystem):
-    name = "chromium-ews"
-    port_name = "chromium"
-    watchers = AbstractEarlyWarningSystem.watchers + [
-        "dglazkov at chromium.org",
-    ]
-
-
-# For platforms that we can't run inside a VM (like Mac OS X), we require
-# patches to be uploaded by committers, who are generally trustworthy folk. :)
-class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem):
-    def __init__(self, committers=CommitterList()):
-        AbstractEarlyWarningSystem.__init__(self)
-        self._committers = committers
-
-    def process_work_item(self, patch):
-        if not self._committers.committer_by_email(patch["attacher_email"]):
-            self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name)
-            return
-        AbstractEarlyWarningSystem.process_work_item(self, patch)
-
-
-class MacEWS(AbstractCommitterOnlyEWS):
-    name = "mac-ews"
-    port_name = "mac"
diff --git a/WebKitTools/Scripts/modules/commands/early_warning_system_unittest.py b/WebKitTools/Scripts/modules/commands/early_warning_system_unittest.py
deleted file mode 100644
index 356e91e..0000000
--- a/WebKitTools/Scripts/modules/commands/early_warning_system_unittest.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import unittest
-
-from modules.commands.early_warning_system import *
-from modules.commands.queuestest import QueuesTest
-from modules.mock import Mock
-
-class EarlyWarningSytemTest(QueuesTest):
-    def test_chromium_ews(self):
-        expected_stderr = {
-            "begin_work_queue" : "CAUTION: chromium-ews will discard all local changes in \"%s\"\nRunning WebKit chromium-ews.\n" % os.getcwd(),
-            "handle_unexpected_error" : "Mock error message\n",
-        }
-        self.assert_queue_outputs(ChromiumEWS(), expected_stderr=expected_stderr)
-
-    def test_qt_ews(self):
-        expected_stderr = {
-            "begin_work_queue" : "CAUTION: qt-ews will discard all local changes in \"%s\"\nRunning WebKit qt-ews.\n" % os.getcwd(),
-            "handle_unexpected_error" : "Mock error message\n",
-        }
-        self.assert_queue_outputs(QtEWS(), expected_stderr=expected_stderr)
-
-    def test_gtk_ews(self):
-        expected_stderr = {
-            "begin_work_queue" : "CAUTION: gtk-ews will discard all local changes in \"%s\"\nRunning WebKit gtk-ews.\n" % os.getcwd(),
-            "handle_unexpected_error" : "Mock error message\n",
-        }
-        self.assert_queue_outputs(GtkEWS(), expected_stderr=expected_stderr)
-
-    def test_mac_ews(self):
-        expected_stderr = {
-            "begin_work_queue" : "CAUTION: mac-ews will discard all local changes in \"%s\"\nRunning WebKit mac-ews.\n" % os.getcwd(),
-            "handle_unexpected_error" : "Mock error message\n",
-        }
-        self.assert_queue_outputs(MacEWS(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/queries.py b/WebKitTools/Scripts/modules/commands/queries.py
deleted file mode 100644
index d37fa06..0000000
--- a/WebKitTools/Scripts/modules/commands/queries.py
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-from optparse import make_option
-
-from modules.buildbot import BuildBot
-from modules.committers import CommitterList
-from modules.webkit_logging import log
-from modules.multicommandtool import Command
-
-
-class BugsToCommit(Command):
-    name = "bugs-to-commit"
-    def __init__(self):
-        Command.__init__(self, "List bugs in the commit-queue")
-
-    def execute(self, options, args, tool):
-        bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
-        for bug_id in bug_ids:
-            print "%s" % bug_id
-
-
-class PatchesToCommit(Command):
-    name = "patches-to-commit"
-    def __init__(self):
-        Command.__init__(self, "List patches in the commit-queue")
-
-    def execute(self, options, args, tool):
-        patches = tool.bugs.queries.fetch_patches_from_commit_queue()
-        log("Patches in commit queue:")
-        for patch in patches:
-            print "%s" % patch["url"]
-
-
-class PatchesToCommitQueue(Command):
-    name = "patches-to-commit-queue"
-    def __init__(self):
-        options = [
-            make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
-        ]
-        Command.__init__(self, "Patches which should be added to the commit queue", options=options)
-
-    @staticmethod
-    def _needs_commit_queue(patch):
-        commit_queue_flag = patch.get("commit-queue")
-        if (commit_queue_flag and commit_queue_flag == '+'): # If it's already cq+, ignore the patch.
-            log("%s already has cq=%s" % (patch["id"], commit_queue_flag))
-            return False
-
-        # We only need to worry about patches from contributers who are not yet committers.
-        committer_record = CommitterList().committer_by_email(patch["attacher_email"])
-        if committer_record:
-            log("%s committer = %s" % (patch["id"], committer_record))
-        return not committer_record
-
-    def execute(self, options, args, tool):
-        patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
-        patches_needing_cq = filter(self._needs_commit_queue, patches)
-        if options.bugs:
-            bugs_needing_cq = map(lambda patch: patch['bug_id'], patches_needing_cq)
-            bugs_needing_cq = sorted(set(bugs_needing_cq))
-            for bug_id in bugs_needing_cq:
-                print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
-        else:
-            for patch in patches_needing_cq:
-                print "%s" % tool.bugs.attachment_url_for_id(patch["id"], action="edit")
-
-
-class PatchesToReview(Command):
-    name = "patches-to-review"
-    def __init__(self):
-        Command.__init__(self, "List patches that are pending review")
-
-    def execute(self, options, args, tool):
-        patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue()
-        log("Patches pending review:")
-        for patch_id in patch_ids:
-            print patch_id
-
-
-class ReviewedPatches(Command):
-    name = "reviewed-patches"
-    def __init__(self):
-        Command.__init__(self, "List r+'d patches on a bug", "BUGID")
-
-    def execute(self, options, args, tool):
-        bug_id = args[0]
-        patches_to_land = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
-        for patch in patches_to_land:
-            print "%s" % patch["url"]
-
-
-class TreeStatus(Command):
-    name = "tree-status"
-    show_in_main_help = True
-    def __init__(self):
-        Command.__init__(self, "Print the status of the %s buildbots" % BuildBot.default_host)
-
-    def execute(self, options, args, tool):
-        for builder in tool.buildbot.builder_statuses():
-            status_string = "ok" if builder["is_green"] else "FAIL"
-            print "%s : %s" % (status_string.ljust(4), builder["name"])
diff --git a/WebKitTools/Scripts/modules/commands/queries_unittest.py b/WebKitTools/Scripts/modules/commands/queries_unittest.py
deleted file mode 100644
index aa5f7f1..0000000
--- a/WebKitTools/Scripts/modules/commands/queries_unittest.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.bugzilla import Bugzilla
-from modules.commands.commandtest import CommandsTest
-from modules.commands.queries import *
-from modules.mock import Mock
-from modules.mock_bugzillatool import MockBugzillaTool
-
-class QueryCommandsTest(CommandsTest):
-    def test_bugs_to_commit(self):
-        self.assert_execute_outputs(BugsToCommit(), None, "42\n75\n")
-
-    def test_patches_to_commit(self):
-        expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
-        expected_stderr = "Patches in commit queue:\n"
-        self.assert_execute_outputs(PatchesToCommit(), None, expected_stdout, expected_stderr)
-
-    def test_patches_to_commit_queue(self):
-        expected_stdout = "http://example.com/197&action=edit\n"
-        expected_stderr = "128 committer = \"Eric Seidel\" <eric at webkit.org>\n"
-        options = Mock()
-        options.bugs = False
-        self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
-
-        expected_stdout = "http://example.com/42\n"
-        options.bugs = True
-        self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
-
-    def test_patches_to_review(self):
-        expected_stdout = "197\n128\n"
-        expected_stderr = "Patches pending review:\n"
-        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr)
-
-    def test_reviewed_patches(self):
-        expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
-        self.assert_execute_outputs(ReviewedPatches(), [42], expected_stdout)
-
-    def test_tree_status(self):
-        expected_stdout = "ok   : Builder1\nok   : Builder2\n"
-        self.assert_execute_outputs(TreeStatus(), None, expected_stdout)
diff --git a/WebKitTools/Scripts/modules/commands/queues.py b/WebKitTools/Scripts/modules/commands/queues.py
deleted file mode 100644
index e14404c..0000000
--- a/WebKitTools/Scripts/modules/commands/queues.py
+++ /dev/null
@@ -1,252 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-
-from datetime import datetime
-from optparse import make_option
-from StringIO import StringIO
-
-from modules.executive import ScriptError
-from modules.grammar import pluralize
-from modules.webkit_logging import error, log
-from modules.multicommandtool import Command
-from modules.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate
-from modules.statusbot import StatusBot
-from modules.stepsequence import StepSequenceErrorHandler
-from modules.queueengine import QueueEngine, QueueEngineDelegate
-
-class AbstractQueue(Command, QueueEngineDelegate):
-    watchers = [
-        "webkit-bot-watchers at googlegroups.com",
-    ]
-
-    _pass_status = "Pass"
-    _fail_status = "Fail"
-    _error_status = "Error"
-
-    def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
-        options_list = (options or []) + [
-            make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
-        ]
-        Command.__init__(self, "Run the %s" % self.name, options=options_list)
-
-    def _cc_watchers(self, bug_id):
-        try:
-            self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
-        except Exception, e:
-            log("Failed to CC watchers: %s." % e)
-
-    def _update_status(self, message, patch=None, results_file=None):
-        self.tool.status_bot.update_status(self.name, message, patch, results_file)
-
-    def _did_pass(self, patch):
-        self._update_status(self._pass_status, patch)
-
-    def _did_fail(self, patch):
-        self._update_status(self._fail_status, patch)
-
-    def _did_error(self, patch, reason):
-        message = "%s: %s" % (self._error_status, reason)
-        self._update_status(message, patch)
-
-    def queue_log_path(self):
-        return "%s.log" % self.name
-
-    def work_item_log_path(self, patch):
-        return os.path.join("%s-logs" % self.name, "%s.log" % patch["bug_id"])
-
-    def begin_work_queue(self):
-        log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root))
-        if self.options.confirm:
-            response = raw_input("Are you sure?  Type \"yes\" to continue: ")
-            if (response != "yes"):
-                error("User declined.")
-        log("Running WebKit %s." % self.name)
-
-    def should_continue_work_queue(self):
-        return True
-
-    def next_work_item(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def should_proceed_with_work_item(self, work_item):
-        raise NotImplementedError, "subclasses must implement"
-
-    def process_work_item(self, work_item):
-        raise NotImplementedError, "subclasses must implement"
-
-    def handle_unexpected_error(self, work_item, message):
-        raise NotImplementedError, "subclasses must implement"
-
-    def run_bugzilla_tool(self, args):
-        bugzilla_tool_args = [self.tool.path()]
-        # FIXME: This is a hack, we should have a more general way to pass global options.
-        bugzilla_tool_args += ["--status-host=%s" % self.tool.status_bot.statusbot_host]
-        bugzilla_tool_args += map(str, args)
-        self.tool.executive.run_and_throw_if_fail(bugzilla_tool_args)
-
-    def log_progress(self, patch_ids):
-        log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
-
-    def execute(self, options, args, tool, engine=QueueEngine):
-        self.options = options
-        self.tool = tool
-        return engine(self.name, self).run()
-
-    @classmethod
-    def _update_status_for_script_error(cls, tool, state, script_error):
-        return tool.status_bot.update_status(cls.name, script_error.message, state["patch"], StringIO(script_error.output))
-
-
-class CommitQueue(AbstractQueue, StepSequenceErrorHandler):
-    name = "commit-queue"
-    def __init__(self):
-        AbstractQueue.__init__(self)
-
-    # AbstractQueue methods
-
-    def begin_work_queue(self):
-        AbstractQueue.begin_work_queue(self)
-
-    def next_work_item(self):
-        patches = self.tool.bugs.queries.fetch_patches_from_commit_queue(reject_invalid_patches=True)
-        if not patches:
-            self._update_status("Empty queue")
-            return None
-        # Only bother logging if we have patches in the queue.
-        self.log_progress([patch['id'] for patch in patches])
-        return patches[0]
-
-    def should_proceed_with_work_item(self, patch):
-        red_builders_names = self.tool.buildbot.red_core_builders_names()
-        if red_builders_names:
-            red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
-            self._update_status("Builders [%s] are red. See http://build.webkit.org" % ", ".join(red_builders_names), None)
-            return False
-        self._update_status("Landing patch", patch)
-        return True
-
-    def process_work_item(self, patch):
-        try:
-            self._cc_watchers(patch["bug_id"])
-            self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--build-style=both", "--quiet", patch["id"]])
-            self._did_pass(patch)
-        except ScriptError, e:
-            self._did_fail(patch)
-            raise e
-
-    def handle_unexpected_error(self, patch, message):
-        self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
-
-    # StepSequenceErrorHandler methods
-
-    @staticmethod
-    def _error_message_for_bug(tool, status_id, script_error):
-        if not script_error.output:
-            return script_error.message_with_output()
-        results_link = tool.status_bot.results_url_for_status(status_id)
-        return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
-
-    @classmethod
-    def handle_script_error(cls, tool, state, script_error):
-        status_id = cls._update_status_for_script_error(tool, state, script_error)
-        tool.bugs.reject_patch_from_commit_queue(state["patch"]["id"], cls._error_message_for_bug(tool, status_id, script_error))
-
-
-class AbstractReviewQueue(AbstractQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler):
-    def __init__(self, options=None):
-        AbstractQueue.__init__(self, options)
-
-    # PersistentPatchCollectionDelegate methods
-
-    def collection_name(self):
-        return self.name
-
-    def fetch_potential_patch_ids(self):
-        return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue()
-
-    def status_server(self):
-        return self.tool.status_bot
-
-    # AbstractQueue methods
-
-    def begin_work_queue(self):
-        AbstractQueue.begin_work_queue(self)
-        self._patches = PersistentPatchCollection(self)
-
-    def next_work_item(self):
-        patch_id = self._patches.next()
-        if patch_id:
-            return self.tool.bugs.fetch_attachment(patch_id)
-        self._update_status("Empty queue")
-
-    def should_proceed_with_work_item(self, patch):
-        raise NotImplementedError, "subclasses must implement"
-
-    def process_work_item(self, patch):
-        raise NotImplementedError, "subclasses must implement"
-
-    def handle_unexpected_error(self, patch, message):
-        log(message)
-
-    # StepSequenceErrorHandler methods
-
-    @classmethod
-    def handle_script_error(cls, tool, state, script_error):
-        log(script_error.message_with_output())
-
-
-class StyleQueue(AbstractReviewQueue):
-    name = "style-queue"
-    def __init__(self):
-        AbstractReviewQueue.__init__(self)
-
-    def should_proceed_with_work_item(self, patch):
-        self._update_status("Checking style", patch)
-        return True
-
-    def process_work_item(self, patch):
-        try:
-            self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
-            message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
-            self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
-            self._did_pass(patch)
-        except ScriptError, e:
-            self._did_fail(patch)
-            raise e
-
-    @classmethod
-    def handle_script_error(cls, tool, state, script_error):
-        status_id = cls._update_status_for_script_error(tool, state, script_error)
-        if not script_error.command_name() == "check-webkit-style":
-            return
-        message = "Attachment %s did not pass %s:\n\n%s" % (state["patch"]["id"], cls.name, script_error.message_with_output(output_limit=3*1024))
-        tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
diff --git a/WebKitTools/Scripts/modules/commands/queues_unittest.py b/WebKitTools/Scripts/modules/commands/queues_unittest.py
deleted file mode 100644
index 8647af9..0000000
--- a/WebKitTools/Scripts/modules/commands/queues_unittest.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import unittest
-
-from modules.commands.commandtest import CommandsTest
-from modules.commands.queues import *
-from modules.commands.queuestest import QueuesTest
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-
-
-class TestQueue(AbstractQueue):
-    name = "test-queue"
-
-
-class AbstractQueueTest(CommandsTest):
-    def _assert_log_progress_output(self, patch_ids, progress_output):
-        OutputCapture().assert_outputs(self, TestQueue().log_progress, [patch_ids], expected_stderr=progress_output)
-
-    def test_log_progress(self):
-        self._assert_log_progress_output([1,2,3], "3 patches in test-queue [1, 2, 3]\n")
-        self._assert_log_progress_output(["1","2","3"], "3 patches in test-queue [1, 2, 3]\n")
-        self._assert_log_progress_output([1], "1 patch in test-queue [1]\n")
-
-    def _assert_run_bugzilla_tool(self, run_args):
-        queue = TestQueue()
-        tool = MockBugzillaTool()
-        queue.bind_to_tool(tool)
-
-        queue.run_bugzilla_tool(run_args)
-        expected_run_args = ["echo", "--status-host=example.com"] + map(str, run_args)
-        tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
-
-    def test_run_bugzilla_tool(self):
-        self._assert_run_bugzilla_tool([1])
-        self._assert_run_bugzilla_tool(["one", 2])
-
-
-class CommitQueueTest(QueuesTest):
-    def test_style_queue(self):
-        expected_stderr = {
-            "begin_work_queue" : "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % os.getcwd(),
-            "next_work_item" : "2 patches in commit-queue [197, 128]\n",
-        }
-        self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr)
-
-
-class StyleQueueTest(QueuesTest):
-    def test_style_queue(self):
-        expected_stderr = {
-            "begin_work_queue" : "CAUTION: style-queue will discard all local changes in \"%s\"\nRunning WebKit style-queue.\n" % os.getcwd(),
-            "handle_unexpected_error" : "Mock error message\n",
-        }
-        self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/queuestest.py b/WebKitTools/Scripts/modules/commands/queuestest.py
deleted file mode 100644
index 6cb22a2..0000000
--- a/WebKitTools/Scripts/modules/commands/queuestest.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.mock import Mock
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-
-
-class MockQueueEngine(object):
-    def __init__(self, name, queue):
-        pass
-
-    def run(self):
-        pass
-
-
-class QueuesTest(unittest.TestCase):
-    mock_work_item = {
-        "id" : 1234,
-        "bug_id" : 345,
-        "attacher_email": "adam at example.com",
-    }
-
-    def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, options=Mock(), tool=MockBugzillaTool()):
-        if not expected_stdout:
-            expected_stdout = {}
-        if not expected_stderr:
-            expected_stderr = {}
-        if not args:
-            args = []
-        if not work_item:
-            work_item = self.mock_work_item
-        options.confirm = False # FIXME: We should have a tool.user that we can mock.
-
-        queue.execute(options, args, tool, engine=MockQueueEngine)
-
-        OutputCapture().assert_outputs(self,
-                queue.queue_log_path,
-                expected_stdout=expected_stdout.get("queue_log_path", ""),
-                expected_stderr=expected_stderr.get("queue_log_path", ""))
-        OutputCapture().assert_outputs(self,
-                queue.work_item_log_path,
-                args=[work_item],
-                expected_stdout=expected_stdout.get("work_item_log_path", ""),
-                expected_stderr=expected_stderr.get("work_item_log_path", ""))
-        OutputCapture().assert_outputs(self,
-                queue.begin_work_queue,
-                expected_stdout=expected_stdout.get("begin_work_queue", ""),
-                expected_stderr=expected_stderr.get("begin_work_queue", ""))
-        OutputCapture().assert_outputs(self,
-                queue.should_continue_work_queue,
-                expected_stdout=expected_stdout.get("should_continue_work_queue", ""), expected_stderr=expected_stderr.get("should_continue_work_queue", ""))
-        OutputCapture().assert_outputs(self,
-                queue.next_work_item,
-                expected_stdout=expected_stdout.get("next_work_item", ""),
-                expected_stderr=expected_stderr.get("next_work_item", ""))
-        OutputCapture().assert_outputs(self,
-                queue.should_proceed_with_work_item,
-                args=[work_item],
-                expected_stdout=expected_stdout.get("should_proceed_with_work_item", ""),
-                expected_stderr=expected_stderr.get("should_proceed_with_work_item", ""))
-        OutputCapture().assert_outputs(self,
-                queue.process_work_item,
-                args=[work_item],
-                expected_stdout=expected_stdout.get("process_work_item", ""),
-                expected_stderr=expected_stderr.get("process_work_item", ""))
-        OutputCapture().assert_outputs(self,
-                queue.handle_unexpected_error,
-                args=[work_item, "Mock error message"],
-                expected_stdout=expected_stdout.get("handle_unexpected_error", ""),
-                expected_stderr=expected_stderr.get("handle_unexpected_error", ""))
diff --git a/WebKitTools/Scripts/modules/commands/upload.py b/WebKitTools/Scripts/modules/commands/upload.py
deleted file mode 100644
index ac7995f..0000000
--- a/WebKitTools/Scripts/modules/commands/upload.py
+++ /dev/null
@@ -1,384 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import re
-import StringIO
-import sys
-
-from optparse import make_option
-
-from modules.bugzilla import parse_bug_id
-from modules.buildsteps import PrepareChangeLogStep, EditChangeLogStep, ConfirmDiffStep, CommandOptions, ObsoletePatchesOnBugStep, PostDiffToBugStep, PromptForBugOrTitleStep, CreateBugStep
-from modules.commands.download import AbstractSequencedCommmand
-from modules.comments import bug_comment_from_svn_revision
-from modules.committers import CommitterList
-from modules.grammar import pluralize
-from modules.webkit_logging import error, log
-from modules.multicommandtool import Command, AbstractDeclarativeCommmand
-
-# FIXME: Requires unit test.
-class CommitMessageForCurrentDiff(Command):
-    name = "commit-message"
-    def __init__(self):
-        Command.__init__(self, "Print a commit message suitable for the uncommitted changes")
-
-    def execute(self, options, args, tool):
-        os.chdir(tool.scm().checkout_root)
-        print "%s" % tool.scm().commit_message_for_this_commit().message()
-
-
-class AssignToCommitter(AbstractDeclarativeCommmand):
-    name = "assign-to-committer"
-    help_text = "Assign bug to whoever attached the most recent r+'d patch"
-
-    def _assign_bug_to_last_patch_attacher(self, bug_id):
-        committers = CommitterList()
-        bug = self.tool.bugs.fetch_bug(bug_id)
-        assigned_to_email = bug.assigned_to_email()
-        if assigned_to_email != self.tool.bugs.unassigned_email:
-            log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
-            return
-
-        # FIXME: This should call a reviewed_patches() method on bug instead of re-fetching.
-        reviewed_patches = self.tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
-        if not reviewed_patches:
-            log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
-            return
-        latest_patch = reviewed_patches[-1]
-        attacher_email = latest_patch["attacher_email"]
-        committer = committers.committer_by_email(attacher_email)
-        if not committer:
-            log("Attacher %s is not a committer.  Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
-            return
-
-        reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch["id"], committer.full_name)
-        self.tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
-
-    def execute(self, options, args, tool):
-        for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
-            self._assign_bug_to_last_patch_attacher(bug_id)
-
-
-class ObsoleteAttachments(AbstractSequencedCommmand):
-    name = "obsolete-attachments"
-    help_text = "Mark all attachments on a bug as obsolete"
-    argument_names = "BUGID"
-    steps = [
-        ObsoletePatchesOnBugStep,
-    ]
-
-    def _prepare_state(self, options, args, tool):
-        return { "bug_id" : args[0] }
-
-
-class PostDiff(AbstractSequencedCommmand):
-    name = "post-diff"
-    help_text = "Attach the current working directory diff to a bug as a patch file"
-    argument_names = "[BUGID]"
-    show_in_main_help = True
-    steps = [
-        ConfirmDiffStep,
-        ObsoletePatchesOnBugStep,
-        PostDiffToBugStep,
-    ]
-
-    def _prepare_state(self, options, args, tool):
-        # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
-        state = {}
-        bug_id = args and args[0]
-        if not bug_id:
-            state["diff"] = tool.scm().create_patch()
-            bug_id = parse_bug_id(state["diff"])
-        if not bug_id:
-            error("No bug id passed and no bug url found in diff, can't post.")
-        state["bug_id"] = bug_id
-        return state
-
-
-class PrepareDiff(AbstractSequencedCommmand):
-    name = "prepare-diff"
-    help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
-    argument_names = "[BUGID]"
-    steps = [
-        PromptForBugOrTitleStep,
-        CreateBugStep,
-        PrepareChangeLogStep,
-    ]
-
-    def _prepare_state(self, options, args, tool):
-        bug_id = args and args[0]
-        return { "bug_id" : bug_id }
-
-
-class CreateReview(AbstractSequencedCommmand):
-    name = "create-review"
-    help_text = "Adds a ChangeLog to the current diff and posts it to a (possibly new) bug"
-    argument_names = "[BUGID]"
-    steps = [
-        PromptForBugOrTitleStep,
-        CreateBugStep,
-        PrepareChangeLogStep,
-        EditChangeLogStep,
-        ConfirmDiffStep,
-        ObsoletePatchesOnBugStep,
-        PostDiffToBugStep,
-    ]
-
-    def _prepare_state(self, options, args, tool):
-        bug_id = args and args[0]
-        return { "bug_id" : bug_id }
-
-
-class EditChangeLog(AbstractSequencedCommmand):
-    name = "edit-changelog"
-    help_text = "Opens modified ChangeLogs in $EDITOR"
-    steps = [
-        EditChangeLogStep,
-    ]
-
-
-class PostCommits(Command):
-    name = "post-commits"
-    show_in_main_help = True
-    def __init__(self):
-        options = [
-            make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
-            make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
-            make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
-            CommandOptions.obsolete_patches,
-            CommandOptions.review,
-            CommandOptions.request_commit,
-        ]
-        Command.__init__(self, "Attach a range of local commits to bugs as patch files", "COMMITISH", options=options, requires_local_commits=True)
-
-    def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
-        comment_text = None
-        if (options.add_log_as_comment):
-            comment_text = commit_message.body(lstrip=True)
-            comment_text += "---\n"
-            comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
-        return comment_text
-
-    def _diff_file_for_commit(self, tool, commit_id):
-        diff = tool.scm().create_patch_from_local_commit(commit_id)
-        return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
-
-    def execute(self, options, args, tool):
-        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
-        if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
-            error("bugzilla-tool does not support attaching %s at once.  Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
-
-        have_obsoleted_patches = set()
-        for commit_id in commit_ids:
-            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
-
-            # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
-            bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch_from_local_commit(commit_id))
-            if not bug_id:
-                log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
-                continue
-
-            if options.obsolete_patches and bug_id not in have_obsoleted_patches:
-                state = { "bug_id": bug_id }
-                ObsoletePatchesOnBugStep(tool, options).run(state)
-                have_obsoleted_patches.add(bug_id)
-
-            diff_file = self._diff_file_for_commit(tool, commit_id)
-            description = options.description or commit_message.description(lstrip=True, strip_url=True)
-            comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
-            tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
-
-
-# FIXME: Requires unit test.  Blocking issue: too complex for now.
-class MarkBugFixed(Command):
-    name = "mark-bug-fixed"
-    show_in_main_help = True
-    def __init__(self):
-        options = [
-            make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
-            make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
-            make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
-            make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
-        ]
-        Command.__init__(self, "Mark the specified bug as fixed", "[SVN_REVISION]", options=options)
-
-    def _fetch_commit_log(self, tool, svn_revision):
-        if not svn_revision:
-            return tool.scm().last_svn_commit_log()
-        return tool.scm().svn_commit_log(svn_revision)
-
-    def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
-        commit_log = self._fetch_commit_log(tool, svn_revision)
-
-        if not bug_id:
-            bug_id = parse_bug_id(commit_log)
-
-        if not svn_revision:
-            match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
-            if match:
-                svn_revision = match.group('svn_revision')
-
-        if not bug_id or not svn_revision:
-            not_found = []
-            if not bug_id:
-                not_found.append("bug id")
-            if not svn_revision:
-                not_found.append("svn revision")
-            error("Could not find %s on command-line or in %s."
-                  % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
-
-        return (bug_id, svn_revision)
-
-    def _open_bug_in_web_browser(self, tool, bug_id):
-        if sys.platform == "darwin":
-            tool.executive.run_command(["open", tool.bugs.short_bug_url_for_bug_id(bug_id)])
-            return
-        log("WARNING: --open is only supported on Mac OS X.")
-
-    def _prompt_user_for_correctness(self, bug_id, svn_revision):
-        answer = raw_input("Is this correct (y/N)? ")
-        if not re.match("^\s*y(es)?", answer, re.IGNORECASE):
-            exit(1)
-
-    def execute(self, options, args, tool):
-        bug_id = options.bug_id
-
-        svn_revision = args and args[0]
-        if svn_revision:
-            if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
-                svn_revision = svn_revision[1:]
-            if not re.match("^[0-9]+$", svn_revision):
-                error("Invalid svn revision: '%s'" % svn_revision)
-
-        needs_prompt = False
-        if not bug_id or not svn_revision:
-            needs_prompt = True
-            (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
-
-        log("Bug: <%s> %s" % (tool.bugs.short_bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
-        log("Revision: %s" % svn_revision)
-
-        if options.open_bug:
-            self._open_bug_in_web_browser(tool, bug_id)
-
-        if needs_prompt:
-            self._prompt_user_for_correctness(bug_id, svn_revision)
-
-        bug_comment = bug_comment_from_svn_revision(svn_revision)
-        if options.comment:
-            bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
-
-        if options.update_only:
-            log("Adding comment to Bug %s." % bug_id)
-            tool.bugs.post_comment_to_bug(bug_id, bug_comment)
-        else:
-            log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
-            tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
-
-
-# FIXME: Requires unit test.  Blocking issue: too complex for now.
-class CreateBug(Command):
-    name = "create-bug"
-    show_in_main_help = True
-    def __init__(self):
-        options = [
-            CommandOptions.cc,
-            CommandOptions.component,
-            make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
-            make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
-            make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
-        ]
-        Command.__init__(self, "Create a bug from local changes or local commits", "[COMMITISH]", options=options)
-
-    def create_bug_from_commit(self, options, args, tool):
-        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
-        if len(commit_ids) > 3:
-            error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
-
-        commit_id = commit_ids[0]
-
-        bug_title = ""
-        comment_text = ""
-        if options.prompt:
-            (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
-        else:
-            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
-            bug_title = commit_message.description(lstrip=True, strip_url=True)
-            comment_text = commit_message.body(lstrip=True)
-            comment_text += "---\n"
-            comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
-
-        diff = tool.scm().create_patch_from_local_commit(commit_id)
-        diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
-        bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
-
-        if bug_id and len(commit_ids) > 1:
-            options.bug_id = bug_id
-            options.obsolete_patches = False
-            # FIXME: We should pass through --no-comment switch as well.
-            PostCommits.execute(self, options, commit_ids[1:], tool)
-
-    def create_bug_from_patch(self, options, args, tool):
-        bug_title = ""
-        comment_text = ""
-        if options.prompt:
-            (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
-        else:
-            commit_message = tool.scm().commit_message_for_this_commit()
-            bug_title = commit_message.description(lstrip=True, strip_url=True)
-            comment_text = commit_message.body(lstrip=True)
-
-        diff = tool.scm().create_patch()
-        diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
-        bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
-
-    def prompt_for_bug_title_and_comment(self):
-        bug_title = raw_input("Bug title: ")
-        print "Bug comment (hit ^D on blank line to end):"
-        lines = sys.stdin.readlines()
-        try:
-            sys.stdin.seek(0, os.SEEK_END)
-        except IOError:
-            # Cygwin raises an Illegal Seek (errno 29) exception when the above
-            # seek() call is made. Ignoring it seems to cause no harm.
-            # FIXME: Figure out a way to get avoid the exception in the first
-            # place.
-            pass
-        comment_text = "".join(lines)
-        return (bug_title, comment_text)
-
-    def execute(self, options, args, tool):
-        if len(args):
-            if (not tool.scm().supports_local_commits()):
-                error("Extra arguments not supported; patch is taken from working directory.")
-            self.create_bug_from_commit(options, args, tool)
-        else:
-            self.create_bug_from_patch(options, args, tool)
diff --git a/WebKitTools/Scripts/modules/commands/upload_unittest.py b/WebKitTools/Scripts/modules/commands/upload_unittest.py
deleted file mode 100644
index bf10740..0000000
--- a/WebKitTools/Scripts/modules/commands/upload_unittest.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.commands.commandtest import CommandsTest
-from modules.commands.upload import *
-from modules.mock_bugzillatool import MockBugzillaTool
-
-class UploadCommandsTest(CommandsTest):
-    def test_assign_to_committer(self):
-        tool = MockBugzillaTool()
-        expected_stderr = "Bug 75 is already assigned to foo at foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n"
-        self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool)
-        tool.bugs.reassign_bug.assert_called_with(42, "eric at webkit.org", "Attachment 128 was posted by a committer and has review+, assigning to Eric Seidel for commit.")
-
-    def test_obsolete_attachments(self):
-        expected_stderr = "Obsoleting 2 old patches on bug 42\n"
-        self.assert_execute_outputs(ObsoleteAttachments(), [42], expected_stderr=expected_stderr)
-
-    def test_post_diff(self):
-        expected_stderr = "Obsoleting 2 old patches on bug 42\n"
-        self.assert_execute_outputs(PostDiff(), [42], expected_stderr=expected_stderr)
-
-    def test_prepare_diff_with_arg(self):
-        self.assert_execute_outputs(PrepareDiff(), [42])
-
-    def test_prepare_diff(self):
-        self.assert_execute_outputs(PrepareDiff(), [])
-
-    def test_create_review(self):
-        expected_stderr = "Obsoleting 2 old patches on bug 42\n"
-        self.assert_execute_outputs(CreateReview(), [42], expected_stderr=expected_stderr)
-
-    def test_edit_changelog(self):
-        self.assert_execute_outputs(EditChangeLog(), [])
diff --git a/WebKitTools/Scripts/modules/comments.py b/WebKitTools/Scripts/modules/comments.py
deleted file mode 100755
index eeee655..0000000
--- a/WebKitTools/Scripts/modules/comments.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# A tool for automating dealing with bugzilla, posting patches, committing patches, etc.
-
-from modules.changelogs import view_source_url
-
-def bug_comment_from_svn_revision(svn_revision):
-    return "Committed r%s: <%s>" % (svn_revision, view_source_url(svn_revision))
-
-def bug_comment_from_commit_text(scm, commit_text):
-    svn_revision = scm.svn_revision_from_commit_text(commit_text)
-    return bug_comment_from_svn_revision(svn_revision)
diff --git a/WebKitTools/Scripts/modules/credentials.py b/WebKitTools/Scripts/modules/credentials.py
deleted file mode 100644
index 5ca5983..0000000
--- a/WebKitTools/Scripts/modules/credentials.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# Copyright (c) 2009 Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# Python module for reading stored web credentials from the OS.
-
-import getpass
-import os
-import platform
-import re
-
-from modules.executive import Executive
-from modules.webkit_logging import log
-from modules.scm import Git
-
-class Credentials(object):
-    keychain_entry_not_found = "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain."
-
-    def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd()):
-        self.host = host
-        self.git_prefix = git_prefix
-        self.executive = executive or Executive()
-        self.cwd = cwd
-
-    def _credentials_from_git(self):
-        return [self._read_git_config("username"), self._read_git_config("password")]
-
-    def _read_git_config(self, key):
-        config_key = "%s.%s" % (self.git_prefix, key) if self.git_prefix else key
-        return self.executive.run_command(["git", "config", "--get", config_key], error_handler=Executive.ignore_error).rstrip('\n')
-
-    def _keychain_value_with_label(self, label, source_text):
-        match = re.search("%s\"(?P<value>.+)\"" % label, source_text, re.MULTILINE)
-        if match:
-            return match.group('value')
-
-    def _is_mac_os_x(self):
-        return platform.mac_ver()[0]
-
-    def _parse_security_tool_output(self, security_output):
-        if security_output == self.keychain_entry_not_found:
-            return [None, None]
-        username = self._keychain_value_with_label("^\s*\"acct\"<blob>=", security_output)
-        password = self._keychain_value_with_label("^password: ", security_output)
-        return [username, password]
-
-    def _run_security_tool(self, username=None):
-        security_command = ["/usr/bin/security", "find-internet-password", "-g", "-s", self.host]
-        if username:
-            security_command += ["-a", username]
-
-        log("Reading Keychain for %s account and password.  Click \"Allow\" to continue..." % self.host)
-        return self.executive.run_command(security_command)
-
-    def _credentials_from_keychain(self, username=None):
-        if not self._is_mac_os_x():
-            return [username, None]
-
-        security_output = self._run_security_tool(username)
-        return self._parse_security_tool_output(security_output)
-
-    def read_credentials(self):
-        username = None
-        password = None
-
-        if Git.in_working_directory(self.cwd):
-            (username, password) = self._credentials_from_git()
-
-        if not username or not password:
-            (username, password) = self._credentials_from_keychain(username)
-
-        if not username:
-            username = raw_input("%s login: " % self.host)
-        if not password:
-            password = getpass.getpass("%s password for %s: " % (self.host, username))
-
-        return [username, password]
diff --git a/WebKitTools/Scripts/modules/credentials_unittest.py b/WebKitTools/Scripts/modules/credentials_unittest.py
deleted file mode 100644
index 0e82932..0000000
--- a/WebKitTools/Scripts/modules/credentials_unittest.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import tempfile
-import unittest
-from modules.credentials import Credentials
-from modules.executive import Executive
-from modules.outputcapture import OutputCapture
-from modules.mock import Mock
-
-class CredentialsTest(unittest.TestCase):
-    example_security_output = """keychain: "/Users/test/Library/Keychains/login.keychain"
-class: "inet"
-attributes:
-    0x00000007 <blob>="bugs.webkit.org (test at webkit.org)"
-    0x00000008 <blob>=<NULL>
-    "acct"<blob>="test at webkit.org"
-    "atyp"<blob>="form"
-    "cdat"<timedate>=0x32303039303832353233353231365A00  "20090825235216Z\000"
-    "crtr"<uint32>=<NULL>
-    "cusi"<sint32>=<NULL>
-    "desc"<blob>="Web form password"
-    "icmt"<blob>="default"
-    "invi"<sint32>=<NULL>
-    "mdat"<timedate>=0x32303039303930393137323635315A00  "20090909172651Z\000"
-    "nega"<sint32>=<NULL>
-    "path"<blob>=<NULL>
-    "port"<uint32>=0x00000000 
-    "prot"<blob>=<NULL>
-    "ptcl"<uint32>="htps"
-    "scrp"<sint32>=<NULL>
-    "sdmn"<blob>=<NULL>
-    "srvr"<blob>="bugs.webkit.org"
-    "type"<uint32>=<NULL>
-password: "SECRETSAUCE"
-"""
-
-    def test_keychain_lookup_on_non_mac(self):
-        class FakeCredentials(Credentials):
-            def _is_mac_os_x(self):
-                return False
-        credentials = FakeCredentials("bugs.webkit.org")
-        self.assertEqual(credentials._is_mac_os_x(), False)
-        self.assertEqual(credentials._credentials_from_keychain("foo"), ["foo", None])
-
-    def test_security_output_parse(self):
-        credentials = Credentials("bugs.webkit.org")
-        self.assertEqual(credentials._parse_security_tool_output(self.example_security_output), ["test at webkit.org", "SECRETSAUCE"])
-
-    def test_security_output_parse_entry_not_found(self):
-        credentials = Credentials("foo.example.com")
-        self.assertEqual(credentials._parse_security_tool_output(Credentials.keychain_entry_not_found), [None, None])
-
-    def _assert_security_call(self, username=None):
-        executive_mock = Mock()
-        credentials = Credentials("example.com", executive=executive_mock)
-
-        expected_stderr = "Reading Keychain for example.com account and password.  Click \"Allow\" to continue...\n"
-        OutputCapture().assert_outputs(self, credentials._run_security_tool, [username], expected_stderr=expected_stderr)
-
-        security_args = ["/usr/bin/security", "find-internet-password", "-g", "-s", "example.com"]
-        if username:
-            security_args += ["-a", username]
-        executive_mock.run_command.assert_called_with(security_args)
-
-    def test_security_calls(self):
-        self._assert_security_call()
-        self._assert_security_call(username="foo")
-
-    def test_git_config_calls(self):
-        executive_mock = Mock()
-        credentials = Credentials("example.com", executive=executive_mock)
-        credentials._read_git_config("foo")
-        executive_mock.run_command.assert_called_with(["git", "config", "--get", "foo"], error_handler=Executive.ignore_error)
-
-        credentials = Credentials("example.com", git_prefix="test_prefix", executive=executive_mock)
-        credentials._read_git_config("foo")
-        executive_mock.run_command.assert_called_with(["git", "config", "--get", "test_prefix.foo"], error_handler=Executive.ignore_error)
-
-    def test_read_credentials_without_git_repo(self):
-        class FakeCredentials(Credentials):
-            def _is_mac_os_x(self):
-                return True
-            def _credentials_from_keychain(self, username):
-                return ["test at webkit.org", "SECRETSAUCE"]
-
-        temp_dir_path = tempfile.mkdtemp(suffix="not_a_git_repo")
-        credentials = FakeCredentials("bugs.webkit.org", cwd=temp_dir_path)
-        self.assertEqual(credentials.read_credentials(), ["test at webkit.org", "SECRETSAUCE"])
-        os.rmdir(temp_dir_path)
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/executive.py b/WebKitTools/Scripts/modules/executive.py
deleted file mode 100644
index 115a8bc..0000000
--- a/WebKitTools/Scripts/modules/executive.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import StringIO
-import subprocess
-import sys
-
-from modules.webkit_logging import tee
-
-
-class ScriptError(Exception):
-    def __init__(self, message=None, script_args=None, exit_code=None, output=None, cwd=None):
-        if not message:
-            message = 'Failed to run "%s"' % script_args
-            if exit_code:
-                message += " exit_code: %d" % exit_code
-            if cwd:
-                message += " cwd: %s" % cwd
-
-        Exception.__init__(self, message)
-        self.script_args = script_args # 'args' is already used by Exception
-        self.exit_code = exit_code
-        self.output = output
-        self.cwd = cwd
-
-    def message_with_output(self, output_limit=500):
-        if self.output:
-            if output_limit and len(self.output) > output_limit:
-                 return "%s\nLast %s characters of output:\n%s" % (self, output_limit, self.output[-output_limit:])
-            return "%s\n%s" % (self, self.output)
-        return str(self)
-
-    def command_name(self):
-        command_path = self.script_args
-        if type(command_path) is list:
-            command_path = command_path[0]
-        return os.path.basename(command_path)
-
-
-# FIXME: This should not be a global static.
-# New code should use Executive.run_command directly instead
-def run_command(*args, **kwargs):
-    return Executive().run_command(*args, **kwargs)
-
-
-class Executive(object):
-    def _run_command_with_teed_output(self, args, teed_output):
-        child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-
-        # Use our own custom wait loop because Popen ignores a tee'd stderr/stdout.
-        # FIXME: This could be improved not to flatten output to stdout.
-        while True:
-            output_line = child_process.stdout.readline()
-            if output_line == "" and child_process.poll() != None:
-                return child_process.poll()
-            teed_output.write(output_line)
-
-    def run_and_throw_if_fail(self, args, quiet=False):
-        # Cache the child's output locally so it can be used for error reports.
-        child_out_file = StringIO.StringIO()
-        if quiet:
-            dev_null = open(os.devnull, "w")
-        child_stdout = tee(child_out_file, dev_null if quiet else sys.stdout)
-        exit_code = self._run_command_with_teed_output(args, child_stdout)
-        if quiet:
-            dev_null.close()
-
-        child_output = child_out_file.getvalue()
-        child_out_file.close()
-
-        if exit_code:
-            raise ScriptError(script_args=args, exit_code=exit_code, output=child_output)
-
-    # Error handlers do not need to be static methods once all callers are updated to use an Executive object.
-    @staticmethod
-    def default_error_handler(error):
-        raise error
-
-    @staticmethod
-    def ignore_error(error):
-        pass
-
-    # FIXME: This should be merged with run_and_throw_if_fail
-    def run_command(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True):
-        if hasattr(input, 'read'): # Check if the input is a file.
-            stdin = input
-            string_to_communicate = None
-        else:
-            stdin = subprocess.PIPE if input else None
-            string_to_communicate = input
-        if return_stderr:
-            stderr = subprocess.STDOUT
-        else:
-            stderr = None
-        try:
-            process = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
-            output = process.communicate(string_to_communicate)[0]
-            exit_code = process.wait()
-        except OSError, e:
-            # Catch OSError exceptions. For example, "no such file or directory" (i.e. OSError errno 2),
-            # when the command cannot be found.
-            output = e.strerror
-            exit_code = e.errno
-        if exit_code:
-            script_error = ScriptError(script_args=args, exit_code=exit_code, output=output, cwd=cwd)
-            (error_handler or self.default_error_handler)(script_error)
-        if return_exit_code:
-            return exit_code
-        return output
diff --git a/WebKitTools/Scripts/modules/executive_unittest.py b/WebKitTools/Scripts/modules/executive_unittest.py
deleted file mode 100644
index 580a2e8..0000000
--- a/WebKitTools/Scripts/modules/executive_unittest.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# Copyright (C) 2009 Daniel Bates (dbates at intudata.com). All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-from modules.executive import Executive, ScriptError, run_command
-
-class ExecutiveTest(unittest.TestCase):
-
-    def test_run_command_with_bad_command_check_return_code(self):
-        self.assertEqual(run_command(["foo_bar_command_blah"], error_handler=Executive.ignore_error, return_exit_code=True), 2)
-
-    def test_run_command_with_bad_command_check_calls_error_handler(self):
-        self.didHandleErrorGetCalled = False
-        def handleError(scriptError):
-            self.didHandleErrorGetCalled = True
-            self.assertEqual(scriptError.exit_code, 2)
-
-        run_command(["foo_bar_command_blah"], error_handler=handleError)
-        self.assertTrue(self.didHandleErrorGetCalled)
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/mock_bugzillatool.py b/WebKitTools/Scripts/modules/mock_bugzillatool.py
deleted file mode 100644
index 533302d..0000000
--- a/WebKitTools/Scripts/modules/mock_bugzillatool.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-
-from modules.mock import Mock
-from modules.scm import CommitMessage
-from modules.bugzilla import Bug
-
-def _id_to_object_dictionary(*objects):
-    dictionary = {}
-    for thing in objects:
-        dictionary[thing["id"]] = thing
-    return dictionary
-
-# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
-_patch1 = {
-    "id" : 197,
-    "bug_id" : 42,
-    "url" : "http://example.com/197",
-    "is_obsolete" : False,
-    "is_patch" : True,
-    "reviewer" : "Reviewer1",
-    "attacher_email" : "Contributer1",
-}
-_patch2 = {
-    "id" : 128,
-    "bug_id" : 42,
-    "url" : "http://example.com/128",
-    "is_obsolete" : False,
-    "is_patch" : True,
-    "reviewer" : "Reviewer2",
-    "attacher_email" : "eric at webkit.org",
-}
-
-# This must be defined before we define the bugs, thus we don't use MockBugzilla.unassigned_email directly.
-_unassigned_email = "unassigned at example.com"
-
-# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
-_bug1 = {
-    "id" : 42,
-    "assigned_to_email" : _unassigned_email,
-    "attachments" : [_patch1, _patch2],
-}
-_bug2 = {
-    "id" : 75,
-    "assigned_to_email" : "foo at foo.com",
-    "attachments" : [],
-}
-_bug3 = {
-    "id" : 76,
-    "assigned_to_email" : _unassigned_email,
-    "attachments" : [],
-}
-
-class MockBugzillaQueries(Mock):
-    def fetch_bug_ids_from_commit_queue(self):
-        return [42, 75]
-
-    def fetch_attachment_ids_from_review_queue(self):
-        return [197, 128]
-
-    def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
-        return [_patch1, _patch2]
-
-    def fetch_bug_ids_from_pending_commit_list(self):
-        return [42, 75, 76]
-    
-    def fetch_patches_from_pending_commit_list(self):
-        return [_patch1, _patch2]
-
-
-class MockBugzilla(Mock):
-    bug_server_url = "http://example.com"
-    unassigned_email = _unassigned_email
-    bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3)
-    attachment_cache = _id_to_object_dictionary(_patch1, _patch2)
-    queries = MockBugzillaQueries()
-
-    def fetch_bug(self, bug_id):
-        return Bug(self.bug_cache.get(bug_id))
-
-    def fetch_reviewed_patches_from_bug(self, bug_id):
-        return self.fetch_patches_from_bug(bug_id) # Return them all for now.
-
-    def fetch_attachment(self, attachment_id):
-        return self.attachment_cache[attachment_id] # This could be changed to .get() if we wish to allow failed lookups.
-
-    # NOTE: Functions below this are direct copies from bugzilla.py
-    def fetch_patches_from_bug(self, bug_id):
-        return self.fetch_bug(bug_id).patches()
-    
-    def bug_url_for_bug_id(self, bug_id):
-        return "%s/%s" % (self.bug_server_url, bug_id)
-
-    def attachment_url_for_id(self, attachment_id, action):
-        action_param = ""
-        if action and action != "view":
-            action_param = "&action=%s" % action
-        return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param)
-
-
-class MockBuildBot(Mock):
-    def builder_statuses(self):
-        return [{
-            "name": "Builder1",
-            "is_green": True
-        }, {
-            "name": "Builder2",
-            "is_green": True
-        }]
-
-    def red_core_builders_names(self):
-        return []
-
-
-class MockSCM(Mock):
-    def __init__(self):
-        Mock.__init__(self)
-        self.checkout_root = os.getcwd()
-
-    def create_patch(self):
-        return "Patch1"
-
-    def commit_ids_from_commitish_arguments(self, args):
-        return ["Commitish1", "Commitish2"]
-
-    def commit_message_for_local_commit(self, commit_id):
-        if commit_id == "Commitish1":
-            return CommitMessage("CommitMessage1\nhttps://bugs.example.org/show_bug.cgi?id=42\n")
-        if commit_id == "Commitish2":
-            return CommitMessage("CommitMessage2\nhttps://bugs.example.org/show_bug.cgi?id=75\n")
-        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
-
-    def create_patch_from_local_commit(self, commit_id):
-        if commit_id == "Commitish1":
-            return "Patch1"
-        if commit_id == "Commitish2":
-            return "Patch2"
-        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
-
-    def diff_for_revision(self, revision):
-        return "DiffForRevision%s\nhttp://bugs.webkit.org/show_bug.cgi?id=12345" % revision
-
-    def modified_changelogs(self):
-        # Ideally we'd return something more interesting here.
-        # The problem is that LandDiff will try to actually read the path from disk!
-        return []
-
-
-class MockUser(object):
-    def prompt(self, message):
-        return "Mock user response"
-
-    def edit(self, files):
-        pass
-
-    def page(self, message):
-        pass
-
-    def confirm(self):
-        return True
-
-
-class MockStatusBot(object):
-    def __init__(self):
-        self.statusbot_host = "example.com"
-
-    def patch_status(self, queue_name, patch_id):
-        return None
-
-    def update_status(self, queue_name, status, patch=None, results_file=None):
-        return 187
-
-
-class MockBugzillaTool():
-    def __init__(self):
-        self.bugs = MockBugzilla()
-        self.buildbot = MockBuildBot()
-        self.executive = Mock()
-        self.user = MockUser()
-        self._scm = MockSCM()
-        self.status_bot = MockStatusBot()
-
-    def scm(self):
-        return self._scm
-
-    def path(self):
-        return "echo"
diff --git a/WebKitTools/Scripts/modules/multicommandtool.py b/WebKitTools/Scripts/modules/multicommandtool.py
deleted file mode 100644
index 215e948..0000000
--- a/WebKitTools/Scripts/modules/multicommandtool.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# MultiCommandTool provides a framework for writing svn-like/git-like tools
-# which are called with the following format:
-# tool-name [global options] command-name [command options]
-
-import sys
-
-from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option
-
-from modules.grammar import pluralize
-from modules.webkit_logging import log
-
-
-class Command(object):
-    name = None
-    show_in_main_help = False
-    def __init__(self, help_text, argument_names=None, options=None, requires_local_commits=False):
-        self.help_text = help_text
-        self.argument_names = argument_names
-        self.required_arguments = self._parse_required_arguments(argument_names)
-        self.options = options
-        self.requires_local_commits = requires_local_commits
-        self.tool = None
-        # option_parser can be overriden by the tool using set_option_parser
-        # This default parser will be used for standalone_help printing.
-        self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
-
-    # This design is slightly awkward, but we need the
-    # the tool to be able to create and modify the option_parser
-    # before it knows what Command to run.
-    def set_option_parser(self, option_parser):
-        self.option_parser = option_parser
-        self._add_options_to_parser()
-
-    def _add_options_to_parser(self):
-        options = self.options or []
-        for option in options:
-            self.option_parser.add_option(option)
-
-    # The tool calls bind_to_tool on each Command after adding it to its list.
-    def bind_to_tool(self, tool):
-        # Command instances can only be bound to one tool at a time.
-        if self.tool and tool != self.tool:
-            raise Exception("Command already bound to tool!")
-        self.tool = tool
-
-    @staticmethod
-    def _parse_required_arguments(argument_names):
-        required_args = []
-        if not argument_names:
-            return required_args
-        split_args = argument_names.split(" ")
-        for argument in split_args:
-            if argument[0] == '[':
-                # For now our parser is rather dumb.  Do some minimal validation that
-                # we haven't confused it.
-                if argument[-1] != ']':
-                    raise Exception("Failure to parse argument string %s.  Argument %s is missing ending ]" % (argument_names, argument))
-            else:
-                required_args.append(argument)
-        return required_args
-
-    def name_with_arguments(self):
-        usage_string = self.name
-        if self.options:
-            usage_string += " [options]"
-        if self.argument_names:
-            usage_string += " " + self.argument_names
-        return usage_string
-
-    def parse_args(self, args):
-        return self.option_parser.parse_args(args)
-
-    def check_arguments_and_execute(self, options, args, tool=None):
-        if len(args) < len(self.required_arguments):
-            log("%s required, %s provided.  Provided: %s  Required: %s\nSee '%s help %s' for usage." % (
-                pluralize("argument", len(self.required_arguments)),
-                pluralize("argument", len(args)),
-                "'%s'" % " ".join(args),
-                " ".join(self.required_arguments),
-                tool.name(),
-                self.name))
-            return 1
-        return self.execute(options, args, tool) or 0
-
-    def standalone_help(self):
-        help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n"
-        help_text += self.option_parser.format_option_help(IndentedHelpFormatter())
-        return help_text
-
-    def execute(self, options, args, tool):
-        raise NotImplementedError, "subclasses must implement"
-
-    # main() exists so that Commands can be turned into stand-alone scripts.
-    # Other parts of the code will likely require modification to work stand-alone.
-    def main(self, args=sys.argv):
-        (options, args) = self.parse_args(args)
-        # Some commands might require a dummy tool
-        return self.check_arguments_and_execute(options, args)
-
-
-# FIXME: This should just be rolled into Command.  help_text and argument_names do not need to be instance variables.
-class AbstractDeclarativeCommmand(Command):
-    help_text = None
-    argument_names = None
-    def __init__(self, options=None):
-        Command.__init__(self, self.help_text, self.argument_names, options)
-
-
-class HelpPrintingOptionParser(OptionParser):
-    def __init__(self, epilog_method=None, *args, **kwargs):
-        self.epilog_method = epilog_method
-        OptionParser.__init__(self, *args, **kwargs)
-
-    def error(self, msg):
-        self.print_usage(sys.stderr)
-        error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
-        # This method is overriden to add this one line to the output:
-        error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name()
-        self.exit(1, error_message)
-
-    # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog
-    # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive).
-    def format_epilog(self, epilog):
-        if self.epilog_method:
-            return "\n%s\n" % self.epilog_method()
-        return ""
-
-
-class HelpCommand(Command):
-    name = "help"
-
-    def __init__(self):
-        options = [
-            make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"),
-        ]
-        Command.__init__(self, "Display information about this program or its subcommands", "[COMMAND]", options=options)
-        self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser.
-
-    def _help_epilog(self):
-        # Only show commands which are relevant to this checkout's SCM system.  Might this be confusing to some users?
-        if self.show_all_commands:
-            epilog = "All %prog commands:\n"
-            relevant_commands = self.tool.commands[:]
-        else:
-            epilog = "Common %prog commands:\n"
-            relevant_commands = filter(self.tool.should_show_in_main_help, self.tool.commands)
-        longest_name_length = max(map(lambda command: len(command.name), relevant_commands))
-        relevant_commands.sort(lambda a, b: cmp(a.name, b.name))
-        command_help_texts = map(lambda command: "   %s   %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands)
-        epilog += "%s\n" % "".join(command_help_texts)
-        epilog += "See '%prog help --all-commands' to list all commands.\n"
-        epilog += "See '%prog help COMMAND' for more information on a specific command.\n"
-        return epilog.replace("%prog", self.tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name().
-
-    # FIXME: This is a hack so that we don't show --all-commands as a global option:
-    def _remove_help_options(self):
-        for option in self.options:
-            self.option_parser.remove_option(option.get_opt_string())
-
-    def execute(self, options, args, tool):
-        if args:
-            command = self.tool.command_by_name(args[0])
-            if command:
-                print command.standalone_help()
-                return 0
-
-        self.show_all_commands = options.show_all_commands
-        self._remove_help_options()
-        self.option_parser.print_help()
-        return 0
-
-
-class MultiCommandTool(object):
-    global_options = None
-
-    def __init__(self, name=None, commands=None):
-        self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name.
-        # Allow the unit tests to disable command auto-discovery.
-        self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
-        self.help_command = self.command_by_name(HelpCommand.name)
-        # Require a help command, even if the manual test list doesn't include one.
-        if not self.help_command:
-            self.help_command = HelpCommand()
-            self.commands.append(self.help_command)
-        for command in self.commands:
-            command.bind_to_tool(self)
-
-    @classmethod
-    def _add_all_subclasses(cls, class_to_crawl, seen_classes):
-        for subclass in class_to_crawl.__subclasses__():
-            if subclass not in seen_classes:
-                seen_classes.add(subclass)
-                cls._add_all_subclasses(subclass, seen_classes)
-
-    @classmethod
-    def _find_all_commands(cls):
-        commands = set()
-        cls._add_all_subclasses(Command, commands)
-        return sorted(commands)
-
-    def name(self):
-        return self._name
-
-    def _create_option_parser(self):
-        usage = "Usage: %prog [options] COMMAND [ARGS]"
-        return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage)
-
-    @staticmethod
-    def _split_command_name_from_args(args):
-        # Assume the first argument which doesn't start with "-" is the command name.
-        command_index = 0
-        for arg in args:
-            if arg[0] != "-":
-                break
-            command_index += 1
-        else:
-            return (None, args[:])
-
-        command = args[command_index]
-        return (command, args[:command_index] + args[command_index + 1:])
-
-    def command_by_name(self, command_name):
-        for command in self.commands:
-            if command_name == command.name:
-                return command
-        return None
-
-    def path(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def should_show_in_main_help(self, command):
-        return command.show_in_main_help
-
-    def should_execute_command(self, command):
-        return True
-
-    def _add_global_options(self, option_parser):
-        global_options = self.global_options or []
-        for option in global_options:
-            option_parser.add_option(option)
-
-    def handle_global_options(self, options):
-        pass
-
-    def main(self, argv=sys.argv):
-        (command_name, args) = self._split_command_name_from_args(argv[1:])
-
-        option_parser = self._create_option_parser()
-        self._add_global_options(option_parser)
-
-        command = self.command_by_name(command_name) or self.help_command
-        if not command:
-            option_parser.error("%s is not a recognized command" % command_name)
-
-        command.set_option_parser(option_parser)
-        (options, args) = command.parse_args(args)
-        self.handle_global_options(options)
-
-        (should_execute, failure_reason) = self.should_execute_command(command)
-        if not should_execute:
-            log(failure_reason)
-            return 0 # FIXME: Should this really be 0?
-
-        return command.check_arguments_and_execute(options, args, self)
diff --git a/WebKitTools/Scripts/modules/multicommandtool_unittest.py b/WebKitTools/Scripts/modules/multicommandtool_unittest.py
deleted file mode 100644
index 7f485a2..0000000
--- a/WebKitTools/Scripts/modules/multicommandtool_unittest.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import sys
-import unittest
-from multicommandtool import MultiCommandTool, Command
-from modules.outputcapture import OutputCapture
-
-from optparse import make_option
-
-class TrivialCommand(Command):
-    name = "trivial"
-    show_in_main_help = True
-    def __init__(self, **kwargs):
-        Command.__init__(self, "help text", **kwargs)
-
-    def execute(self, options, args, tool):
-        pass
-
-class UncommonCommand(TrivialCommand):
-    name = "uncommon"
-    show_in_main_help = False
-
-class CommandTest(unittest.TestCase):
-    def test_name_with_arguments(self):
-        command_with_args = TrivialCommand(argument_names="ARG1 ARG2")
-        self.assertEqual(command_with_args.name_with_arguments(), "trivial ARG1 ARG2")
-
-        command_with_args = TrivialCommand(options=[make_option("--my_option")])
-        self.assertEqual(command_with_args.name_with_arguments(), "trivial [options]")
-
-    def test_parse_required_arguments(self):
-        self.assertEqual(Command._parse_required_arguments("ARG1 ARG2"), ["ARG1", "ARG2"])
-        self.assertEqual(Command._parse_required_arguments("[ARG1] [ARG2]"), [])
-        self.assertEqual(Command._parse_required_arguments("[ARG1] ARG2"), ["ARG2"])
-        # Note: We might make our arg parsing smarter in the future and allow this type of arguments string.
-        self.assertRaises(Exception, Command._parse_required_arguments, "[ARG1 ARG2]")
-
-    def test_required_arguments(self):
-        two_required_arguments = TrivialCommand(argument_names="ARG1 ARG2 [ARG3]")
-        expected_missing_args_error = "2 arguments required, 1 argument provided.  Provided: 'foo'  Required: ARG1 ARG2\nSee 'trivial-tool help trivial' for usage.\n"
-        exit_code = OutputCapture().assert_outputs(self, two_required_arguments.check_arguments_and_execute, [None, ["foo"], TrivialTool()], expected_stderr=expected_missing_args_error)
-        self.assertEqual(exit_code, 1)
-
-
-class TrivialTool(MultiCommandTool):
-    def __init__(self, commands=None):
-        MultiCommandTool.__init__(self, name="trivial-tool", commands=commands)
-
-    def path():
-        return __file__
-
-    def should_execute_command(self, command):
-        return (True, None)
-
-
-class MultiCommandToolTest(unittest.TestCase):
-    def _assert_split(self, args, expected_split):
-        self.assertEqual(MultiCommandTool._split_command_name_from_args(args), expected_split)
-
-    def test_split_args(self):
-        # MultiCommandToolTest._split_command_name_from_args returns: (command, args)
-        full_args = ["--global-option", "command", "--option", "arg"]
-        full_args_expected = ("command", ["--global-option", "--option", "arg"])
-        self._assert_split(full_args, full_args_expected)
-
-        full_args = []
-        full_args_expected = (None, [])
-        self._assert_split(full_args, full_args_expected)
-
-        full_args = ["command", "arg"]
-        full_args_expected = ("command", ["arg"])
-        self._assert_split(full_args, full_args_expected)
-
-    def test_command_by_name(self):
-        # This also tests Command auto-discovery.
-        tool = TrivialTool()
-        self.assertEqual(tool.command_by_name("trivial").name, "trivial")
-        self.assertEqual(tool.command_by_name("bar"), None)
-
-    def _assert_tool_main_outputs(self, tool, main_args, expected_stdout, expected_stderr = "", expected_exit_code=0):
-        exit_code = OutputCapture().assert_outputs(self, tool.main, [main_args], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
-        self.assertEqual(exit_code, expected_exit_code)
-
-    def test_global_help(self):
-        tool = TrivialTool(commands=[TrivialCommand(), UncommonCommand()])
-        expected_common_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
-
-Options:
-  -h, --help  show this help message and exit
-
-Common trivial-tool commands:
-   trivial   help text
-
-See 'trivial-tool help --all-commands' to list all commands.
-See 'trivial-tool help COMMAND' for more information on a specific command.
-
-"""
-        self._assert_tool_main_outputs(tool, ["tool"], expected_common_commands_help)
-        self._assert_tool_main_outputs(tool, ["tool", "help"], expected_common_commands_help)
-        expected_all_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
-
-Options:
-  -h, --help  show this help message and exit
-
-All trivial-tool commands:
-   help       Display information about this program or its subcommands
-   trivial    help text
-   uncommon   help text
-
-See 'trivial-tool help --all-commands' to list all commands.
-See 'trivial-tool help COMMAND' for more information on a specific command.
-
-"""
-        self._assert_tool_main_outputs(tool, ["tool", "help", "--all-commands"], expected_all_commands_help)
-        # Test that arguments can be passed before commands as well
-        self._assert_tool_main_outputs(tool, ["tool", "--all-commands", "help"], expected_all_commands_help)
-
-
-    def test_command_help(self):
-        command_with_options = TrivialCommand(options=[make_option("--my_option")])
-        tool = TrivialTool(commands=[command_with_options])
-        expected_subcommand_help = "trivial [options]   help text\nOptions:\n  --my_option=MY_OPTION\n\n"
-        self._assert_tool_main_outputs(tool, ["tool", "help", "trivial"], expected_subcommand_help)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/queueengine.py b/WebKitTools/Scripts/modules/queueengine.py
deleted file mode 100644
index 8640f02..0000000
--- a/WebKitTools/Scripts/modules/queueengine.py
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import time
-import traceback
-
-from datetime import datetime, timedelta
-
-from modules.executive import ScriptError
-from modules.webkit_logging import log, OutputTee
-from modules.statusbot import StatusBot
-
-class QueueEngineDelegate:
-    def queue_log_path(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def work_item_log_path(self, work_item):
-        raise NotImplementedError, "subclasses must implement"
-
-    def begin_work_queue(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def should_continue_work_queue(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def next_work_item(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def should_proceed_with_work_item(self, work_item):
-        # returns (safe_to_proceed, waiting_message, patch)
-        raise NotImplementedError, "subclasses must implement"
-
-    def process_work_item(self, work_item):
-        raise NotImplementedError, "subclasses must implement"
-
-    def handle_unexpected_error(self, work_item, message):
-        raise NotImplementedError, "subclasses must implement"
-
-
-class QueueEngine:
-    def __init__(self, name, delegate):
-        self._name = name
-        self._delegate = delegate
-        self._output_tee = OutputTee()
-
-    log_date_format = "%Y-%m-%d %H:%M:%S"
-    sleep_duration_text = "5 mins"
-    seconds_to_sleep = 300
-    handled_error_code = 2
-
-    # Child processes exit with a special code to the parent queue process can detect the error was handled.
-    @classmethod
-    def exit_after_handled_error(cls, error):
-        log(error)
-        exit(cls.handled_error_code)
-
-    def run(self):
-        self._begin_logging()
-
-        self._delegate.begin_work_queue()
-        while (self._delegate.should_continue_work_queue()):
-            try:
-                self._ensure_work_log_closed()
-                work_item = self._delegate.next_work_item()
-                if not work_item:
-                    self._sleep("No work item.")
-                    continue
-                if not self._delegate.should_proceed_with_work_item(work_item):
-                    self._sleep("Not proceeding with work item.")
-                    continue
-
-                # FIXME: Work logs should not depend on bug_id specificaly.
-                #        This looks fixed, no?
-                self._open_work_log(work_item)
-                try:
-                    self._delegate.process_work_item(work_item)
-                except ScriptError, e:
-                    # Use a special exit code to indicate that the error was already
-                    # handled in the child process and we should just keep looping.
-                    if e.exit_code == self.handled_error_code:
-                        continue
-                    message = "Unexpected failure when landing patch!  Please file a bug against bugzilla-tool.\n%s" % e.message_with_output()
-                    self._delegate.handle_unexpected_error(work_item, message)
-            except KeyboardInterrupt, e:
-                log("\nUser terminated queue.")
-                return 1
-            except Exception, e:
-                traceback.print_exc()
-                # Don't try tell the status bot, in case telling it causes an exception.
-                self._sleep("Exception while preparing queue: %s." % e)
-        # Never reached.
-        self._ensure_work_log_closed()
-
-    def _begin_logging(self):
-        self._queue_log = self._output_tee.add_log(self._delegate.queue_log_path())
-        self._work_log = None
-
-    def _open_work_log(self, work_item):
-        work_item_log_path = self._delegate.work_item_log_path(work_item)
-        self._work_log = self._output_tee.add_log(work_item_log_path)
-
-    def _ensure_work_log_closed(self):
-        # If we still have a bug log open, close it.
-        if self._work_log:
-            self._output_tee.remove_log(self._work_log)
-            self._work_log = None
-
-    @classmethod
-    def _sleep_message(cls, message):
-        wake_time = datetime.now() + timedelta(seconds=cls.seconds_to_sleep)
-        return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(cls.log_date_format), cls.sleep_duration_text)
-
-    @classmethod
-    def _sleep(cls, message):
-        log(cls._sleep_message(message))
-        time.sleep(cls.seconds_to_sleep)
diff --git a/WebKitTools/Scripts/modules/queueengine_unittest.py b/WebKitTools/Scripts/modules/queueengine_unittest.py
deleted file mode 100644
index 8f1093d..0000000
--- a/WebKitTools/Scripts/modules/queueengine_unittest.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import shutil
-import tempfile
-import unittest
-
-from modules.executive import ScriptError
-from modules.queueengine import QueueEngine, QueueEngineDelegate
-
-class LoggingDelegate(QueueEngineDelegate):
-    def __init__(self, test):
-        self._test = test
-        self._callbacks = []
-        self._run_before = False
-
-    expected_callbacks = [
-        'queue_log_path',
-        'begin_work_queue',
-        'should_continue_work_queue',
-        'next_work_item',
-        'should_proceed_with_work_item',
-        'work_item_log_path',
-        'process_work_item',
-        'should_continue_work_queue'
-    ]
-
-    def record(self, method_name):
-        self._callbacks.append(method_name)
-
-    def queue_log_path(self):
-        self.record("queue_log_path")
-        return os.path.join(self._test.temp_dir, "queue_log_path")
-
-    def work_item_log_path(self, work_item):
-        self.record("work_item_log_path")
-        return os.path.join(self._test.temp_dir, "work_log_path", "%s.log" % work_item)
-
-    def begin_work_queue(self):
-        self.record("begin_work_queue")
-
-    def should_continue_work_queue(self):
-        self.record("should_continue_work_queue")
-        if not self._run_before:
-            self._run_before = True
-            return True
-        return False
-
-    def next_work_item(self):
-        self.record("next_work_item")
-        return "work_item"
-
-    def should_proceed_with_work_item(self, work_item):
-        self.record("should_proceed_with_work_item")
-        self._test.assertEquals(work_item, "work_item")
-        fake_patch = { 'bug_id' : 42 }
-        return (True, "waiting_message", fake_patch)
-
-    def process_work_item(self, work_item):
-        self.record("process_work_item")
-        self._test.assertEquals(work_item, "work_item")
-
-    def handle_unexpected_error(self, work_item, message):
-        self.record("handle_unexpected_error")
-        self._test.assertEquals(work_item, "work_item")
-
-
-class ThrowErrorDelegate(LoggingDelegate):
-    def __init__(self, test, error_code):
-        LoggingDelegate.__init__(self, test)
-        self.error_code = error_code
-
-    def process_work_item(self, work_item):
-        self.record("process_work_item")
-        raise ScriptError(exit_code=self.error_code)
-
-
-class NotSafeToProceedDelegate(LoggingDelegate):
-    def should_proceed_with_work_item(self, work_item):
-        self.record("should_proceed_with_work_item")
-        self._test.assertEquals(work_item, "work_item")
-        return False
-
-
-class FastQueueEngine(QueueEngine):
-    def __init__(self, delegate):
-        QueueEngine.__init__(self, "fast-queue", delegate)
-
-    # No sleep for the wicked.
-    seconds_to_sleep = 0
-
-    def _sleep(self, message):
-        pass
-
-
-class QueueEngineTest(unittest.TestCase):
-    def test_trivial(self):
-        delegate = LoggingDelegate(self)
-        work_queue = QueueEngine("trivial-queue", delegate)
-        work_queue.run()
-        self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
-        self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "queue_log_path")))
-        self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "work_log_path", "work_item.log")))
-
-    def test_unexpected_error(self):
-        delegate = ThrowErrorDelegate(self, 3)
-        work_queue = QueueEngine("error-queue", delegate)
-        work_queue.run()
-        expected_callbacks = LoggingDelegate.expected_callbacks[:]
-        work_item_index = expected_callbacks.index('process_work_item')
-        # The unexpected error should be handled right after process_work_item starts
-        # but before any other callback.  Otherwise callbacks should be normal.
-        expected_callbacks.insert(work_item_index + 1, 'handle_unexpected_error')
-        self.assertEquals(delegate._callbacks, expected_callbacks)
-
-    def test_handled_error(self):
-        delegate = ThrowErrorDelegate(self, QueueEngine.handled_error_code)
-        work_queue = QueueEngine("handled-error-queue", delegate)
-        work_queue.run()
-        self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
-
-    def test_not_safe_to_proceed(self):
-        delegate = NotSafeToProceedDelegate(self)
-        work_queue = FastQueueEngine(delegate)
-        work_queue.run()
-        expected_callbacks = LoggingDelegate.expected_callbacks[:]
-        next_work_item_index = expected_callbacks.index('next_work_item')
-        # We slice out the common part of the expected callbacks.
-        # We add 2 here to include should_proceed_with_work_item, which is
-        # a pain to search for directly because it occurs twice.
-        expected_callbacks = expected_callbacks[:next_work_item_index + 2]
-        expected_callbacks.append('should_continue_work_queue')
-        self.assertEquals(delegate._callbacks, expected_callbacks)
-
-    def setUp(self):
-        self.temp_dir = tempfile.mkdtemp(suffix="work_queue_test_logs")
-
-    def tearDown(self):
-        shutil.rmtree(self.temp_dir)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/scm.py b/WebKitTools/Scripts/modules/scm.py
deleted file mode 100644
index 3ef3093..0000000
--- a/WebKitTools/Scripts/modules/scm.py
+++ /dev/null
@@ -1,512 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# Python module for interacting with an SCM system (like SVN or Git)
-
-import os
-import re
-import subprocess
-
-# Import WebKit-specific modules.
-from modules.changelogs import ChangeLog
-from modules.executive import Executive, run_command, ScriptError
-from modules.webkit_logging import error, log
-
-def detect_scm_system(path):
-    if SVN.in_working_directory(path):
-        return SVN(cwd=path)
-    
-    if Git.in_working_directory(path):
-        return Git(cwd=path)
-    
-    return None
-
-def first_non_empty_line_after_index(lines, index=0):
-    first_non_empty_line = index
-    for line in lines[index:]:
-        if re.match("^\s*$", line):
-            first_non_empty_line += 1
-        else:
-            break
-    return first_non_empty_line
-
-
-class CommitMessage:
-    def __init__(self, message):
-        self.message_lines = message[first_non_empty_line_after_index(message, 0):]
-
-    def body(self, lstrip=False):
-        lines = self.message_lines[first_non_empty_line_after_index(self.message_lines, 1):]
-        if lstrip:
-            lines = [line.lstrip() for line in lines]
-        return "\n".join(lines) + "\n"
-
-    def description(self, lstrip=False, strip_url=False):
-        line = self.message_lines[0]
-        if lstrip:
-            line = line.lstrip()
-        if strip_url:
-            line = re.sub("^(\s*)<.+> ", "\1", line)
-        return line
-
-    def message(self):
-        return "\n".join(self.message_lines) + "\n"
-
-
-class CheckoutNeedsUpdate(ScriptError):
-    def __init__(self, script_args, exit_code, output, cwd):
-        ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd)
-
-
-def commit_error_handler(error):
-    if re.search("resource out of date", error.output):
-        raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd)
-    Executive.default_error_handler(error)
-
-
-class SCM:
-    def __init__(self, cwd, dryrun=False):
-        self.cwd = cwd
-        self.checkout_root = self.find_checkout_root(self.cwd)
-        self.dryrun = dryrun
-
-    def scripts_directory(self):
-        return os.path.join(self.checkout_root, "WebKitTools", "Scripts")
-
-    def script_path(self, script_name):
-        return os.path.join(self.scripts_directory(), script_name)
-
-    def ensure_clean_working_directory(self, force_clean):
-        if not force_clean and not self.working_directory_is_clean():
-            print run_command(self.status_command(), error_handler=Executive.ignore_error)
-            raise ScriptError(message="Working directory has modifications, pass --force-clean or --no-clean to continue.")
-        
-        log("Cleaning working directory")
-        self.clean_working_directory()
-    
-    def ensure_no_local_commits(self, force):
-        if not self.supports_local_commits():
-            return
-        commits = self.local_commits()
-        if not len(commits):
-            return
-        if not force:
-            error("Working directory has local commits, pass --force-clean to continue.")
-        self.discard_local_commits()
-
-    def apply_patch(self, patch, force=False):
-        # It's possible that the patch was not made from the root directory.
-        # We should detect and handle that case.
-        curl_process = subprocess.Popen(['curl', '--location', '--silent', '--show-error', patch['url']], stdout=subprocess.PIPE)
-        args = [self.script_path('svn-apply')]
-        if patch.get('reviewer'):
-            args += ['--reviewer', patch['reviewer']]
-        if force:
-            args.append('--force')
-
-        run_command(args, input=curl_process.stdout)
-
-    def run_status_and_extract_filenames(self, status_command, status_regexp):
-        filenames = []
-        for line in run_command(status_command).splitlines():
-            match = re.search(status_regexp, line)
-            if not match:
-                continue
-            # status = match.group('status')
-            filename = match.group('filename')
-            filenames.append(filename)
-        return filenames
-
-    def strip_r_from_svn_revision(self, svn_revision):
-        match = re.match("^r(?P<svn_revision>\d+)", svn_revision)
-        if (match):
-            return match.group('svn_revision')
-        return svn_revision
-
-    def svn_revision_from_commit_text(self, commit_text):
-        match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE)
-        return match.group('svn_revision')
-
-    # ChangeLog-specific code doesn't really belong in scm.py, but this function is very useful.
-    def modified_changelogs(self):
-        changelog_paths = []
-        paths = self.changed_files()
-        for path in paths:
-            if os.path.basename(path) == "ChangeLog":
-                changelog_paths.append(path)
-        return changelog_paths
-
-    # FIXME: Requires unit test
-    # FIXME: commit_message_for_this_commit and modified_changelogs don't
-    #        really belong here.  We should have a separate module for
-    #        handling ChangeLogs.
-    def commit_message_for_this_commit(self):
-        changelog_paths = self.modified_changelogs()
-        if not len(changelog_paths):
-            raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
-                              "All changes require a ChangeLog.  See:\n"
-                              "http://webkit.org/coding/contributing.html")
-
-        changelog_messages = []
-        for changelog_path in changelog_paths:
-            log("Parsing ChangeLog: %s" % changelog_path)
-            changelog_entry = ChangeLog(changelog_path).latest_entry()
-            if not changelog_entry:
-                raise ScriptError(message="Failed to parse ChangeLog: " + os.path.abspath(changelog_path))
-            changelog_messages.append(changelog_entry)
-
-        # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
-        return CommitMessage("".join(changelog_messages).splitlines())
-
-    @staticmethod
-    def in_working_directory(path):
-        raise NotImplementedError, "subclasses must implement"
-
-    @staticmethod
-    def find_checkout_root(path):
-        raise NotImplementedError, "subclasses must implement"
-
-    @staticmethod
-    def commit_success_regexp():
-        raise NotImplementedError, "subclasses must implement"
-
-    def working_directory_is_clean(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def clean_working_directory(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def status_command(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def changed_files(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def display_name(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def create_patch(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    def diff_for_revision(self, revision):
-        raise NotImplementedError, "subclasses must implement"
-
-    def apply_reverse_diff(self, revision):
-        raise NotImplementedError, "subclasses must implement"
-
-    def revert_files(self, file_paths):
-        raise NotImplementedError, "subclasses must implement"
-
-    def commit_with_message(self, message):
-        raise NotImplementedError, "subclasses must implement"
-
-    def svn_commit_log(self, svn_revision):
-        raise NotImplementedError, "subclasses must implement"
-
-    def last_svn_commit_log(self):
-        raise NotImplementedError, "subclasses must implement"
-
-    # Subclasses must indicate if they support local commits,
-    # but the SCM baseclass will only call local_commits methods when this is true.
-    @staticmethod
-    def supports_local_commits():
-        raise NotImplementedError, "subclasses must implement"
-
-    def create_patch_from_local_commit(self, commit_id):
-        error("Your source control manager does not support creating a patch from a local commit.")
-
-    def create_patch_since_local_commit(self, commit_id):
-        error("Your source control manager does not support creating a patch from a local commit.")
-
-    def commit_locally_with_message(self, message):
-        error("Your source control manager does not support local commits.")
-
-    def discard_local_commits(self):
-        pass
-
-    def local_commits(self):
-        return []
-
-
-class SVN(SCM):
-    def __init__(self, cwd, dryrun=False):
-        SCM.__init__(self, cwd, dryrun)
-        self.cached_version = None
-    
-    @staticmethod
-    def in_working_directory(path):
-        return os.path.isdir(os.path.join(path, '.svn'))
-    
-    @classmethod
-    def find_uuid(cls, path):
-        if not cls.in_working_directory(path):
-            return None
-        return cls.value_from_svn_info(path, 'Repository UUID')
-
-    @classmethod
-    def value_from_svn_info(cls, path, field_name):
-        svn_info_args = ['svn', 'info', path]
-        info_output = run_command(svn_info_args).rstrip()
-        match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)
-        if not match:
-            raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)
-        return match.group('value')
-
-    @staticmethod
-    def find_checkout_root(path):
-        uuid = SVN.find_uuid(path)
-        # If |path| is not in a working directory, we're supposed to return |path|.
-        if not uuid:
-            return path
-        # Search up the directory hierarchy until we find a different UUID.
-        last_path = None
-        while True:
-            if uuid != SVN.find_uuid(path):
-                return last_path
-            last_path = path
-            (path, last_component) = os.path.split(path)
-            if last_path == path:
-                return None
-
-    @staticmethod
-    def commit_success_regexp():
-        return "^Committed revision (?P<svn_revision>\d+)\.$"
-
-    def svn_version(self):
-        if not self.cached_version:
-            self.cached_version = run_command(['svn', '--version', '--quiet'])
-        
-        return self.cached_version
-
-    def working_directory_is_clean(self):
-        return run_command(['svn', 'diff']) == ""
-
-    def clean_working_directory(self):
-        run_command(['svn', 'revert', '-R', '.'])
-
-    def status_command(self):
-        return ['svn', 'status']
-
-    def changed_files(self):
-        if self.svn_version() > "1.6":
-            status_regexp = "^(?P<status>[ACDMR]).{6} (?P<filename>.+)$"
-        else:
-            status_regexp = "^(?P<status>[ACDMR]).{5} (?P<filename>.+)$"
-        return self.run_status_and_extract_filenames(self.status_command(), status_regexp)
-
-    @staticmethod
-    def supports_local_commits():
-        return False
-
-    def display_name(self):
-        return "svn"
-
-    def create_patch(self):
-        return run_command(self.script_path("svn-create-patch"), cwd=self.checkout_root, return_stderr=False)
-
-    def diff_for_revision(self, revision):
-        return run_command(['svn', 'diff', '-c', str(revision)])
-
-    def _repository_url(self):
-        return self.value_from_svn_info(self.checkout_root, 'URL')
-
-    def apply_reverse_diff(self, revision):
-        # '-c -revision' applies the inverse diff of 'revision'
-        svn_merge_args = ['svn', 'merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()]
-        log("WARNING: svn merge has been known to take more than 10 minutes to complete.  It is recommended you use git for rollouts.")
-        log("Running '%s'" % " ".join(svn_merge_args))
-        run_command(svn_merge_args)
-
-    def revert_files(self, file_paths):
-        run_command(['svn', 'revert'] + file_paths)
-
-    def commit_with_message(self, message):
-        if self.dryrun:
-            # Return a string which looks like a commit so that things which parse this output will succeed.
-            return "Dry run, no commit.\nCommitted revision 0."
-        return run_command(['svn', 'commit', '-m', message], error_handler=commit_error_handler)
-
-    def svn_commit_log(self, svn_revision):
-        svn_revision = self.strip_r_from_svn_revision(str(svn_revision))
-        return run_command(['svn', 'log', '--non-interactive', '--revision', svn_revision]);
-
-    def last_svn_commit_log(self):
-        # BASE is the checkout revision, HEAD is the remote repository revision
-        # http://svnbook.red-bean.com/en/1.0/ch03s03.html
-        return self.svn_commit_log('BASE')
-
-# All git-specific logic should go here.
-class Git(SCM):
-    def __init__(self, cwd, dryrun=False):
-        SCM.__init__(self, cwd, dryrun)
-
-    @classmethod
-    def in_working_directory(cls, path):
-        return run_command(['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true"
-
-    @classmethod
-    def find_checkout_root(cls, path):
-        # "git rev-parse --show-cdup" would be another way to get to the root
-        (checkout_root, dot_git) = os.path.split(run_command(['git', 'rev-parse', '--git-dir'], cwd=path))
-        # If we were using 2.6 # checkout_root = os.path.relpath(checkout_root, path)
-        if not os.path.isabs(checkout_root): # Sometimes git returns relative paths
-            checkout_root = os.path.join(path, checkout_root)
-        return checkout_root
-    
-    @staticmethod
-    def commit_success_regexp():
-        return "^Committed r(?P<svn_revision>\d+)$"
-
-
-    def discard_local_commits(self):
-        run_command(['git', 'reset', '--hard', 'trunk'])
-    
-    def local_commits(self):
-        return run_command(['git', 'log', '--pretty=oneline', 'HEAD...trunk']).splitlines()
-
-    def rebase_in_progress(self):
-        return os.path.exists(os.path.join(self.checkout_root, '.git/rebase-apply'))
-
-    def working_directory_is_clean(self):
-        return run_command(['git', 'diff-index', 'HEAD']) == ""
-
-    def clean_working_directory(self):
-        # Could run git clean here too, but that wouldn't match working_directory_is_clean
-        run_command(['git', 'reset', '--hard', 'HEAD'])
-        # Aborting rebase even though this does not match working_directory_is_clean
-        if self.rebase_in_progress():
-            run_command(['git', 'rebase', '--abort'])
-
-    def status_command(self):
-        return ['git', 'status']
-
-    def changed_files(self):
-        status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', 'HEAD']
-        status_regexp = '^(?P<status>[ADM])\t(?P<filename>.+)$'
-        return self.run_status_and_extract_filenames(status_command, status_regexp)
-    
-    @staticmethod
-    def supports_local_commits():
-        return True
-
-    def display_name(self):
-        return "git"
-
-    def create_patch(self):
-        return run_command(['git', 'diff', '--binary', 'HEAD'])
-
-    @classmethod
-    def git_commit_from_svn_revision(cls, revision):
-        # git svn find-rev always exits 0, even when the revision is not found.
-        return run_command(['git', 'svn', 'find-rev', 'r%s' % revision]).rstrip()
-
-    def diff_for_revision(self, revision):
-        git_commit = self.git_commit_from_svn_revision(revision)
-        return self.create_patch_from_local_commit(git_commit)
-
-    def apply_reverse_diff(self, revision):
-        # Assume the revision is an svn revision.
-        git_commit = self.git_commit_from_svn_revision(revision)
-        if not git_commit:
-            raise ScriptError(message='Failed to find git commit for revision %s, git svn log output: "%s"' % (revision, git_commit))
-
-        # I think this will always fail due to ChangeLogs.
-        # FIXME: We need to detec specific failure conditions and handle them.
-        run_command(['git', 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error)
-
-        # Fix any ChangeLogs if necessary.
-        changelog_paths = self.modified_changelogs()
-        if len(changelog_paths):
-            run_command([self.script_path('resolve-ChangeLogs')] + changelog_paths)
-
-    def revert_files(self, file_paths):
-        run_command(['git', 'checkout', 'HEAD'] + file_paths)
-
-    def commit_with_message(self, message):
-        self.commit_locally_with_message(message)
-        return self.push_local_commits_to_server()
-
-    def svn_commit_log(self, svn_revision):
-        svn_revision = self.strip_r_from_svn_revision(svn_revision)
-        return run_command(['git', 'svn', 'log', '-r', svn_revision])
-
-    def last_svn_commit_log(self):
-        return run_command(['git', 'svn', 'log', '--limit=1'])
-
-    # Git-specific methods:
-
-    def create_patch_from_local_commit(self, commit_id):
-        return run_command(['git', 'diff', '--binary', commit_id + "^.." + commit_id])
-
-    def create_patch_since_local_commit(self, commit_id):
-        return run_command(['git', 'diff', '--binary', commit_id])
-
-    def commit_locally_with_message(self, message):
-        run_command(['git', 'commit', '--all', '-F', '-'], input=message)
-        
-    def push_local_commits_to_server(self):
-        if self.dryrun:
-            # Return a string which looks like a commit so that things which parse this output will succeed.
-            return "Dry run, no remote commit.\nCommitted r0"
-        return run_command(['git', 'svn', 'dcommit'], error_handler=commit_error_handler)
-
-    # This function supports the following argument formats:
-    # no args : rev-list trunk..HEAD
-    # A..B    : rev-list A..B
-    # A...B   : error!
-    # A B     : [A, B]  (different from git diff, which would use "rev-list A..B")
-    def commit_ids_from_commitish_arguments(self, args):
-        if not len(args):
-            # FIXME: trunk is not always the remote branch name, need a way to detect the name.
-            args.append('trunk..HEAD')
-
-        commit_ids = []
-        for commitish in args:
-            if '...' in commitish:
-                raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish)
-            elif '..' in commitish:
-                commit_ids += reversed(run_command(['git', 'rev-list', commitish]).splitlines())
-            else:
-                # Turn single commits or branch or tag names into commit ids.
-                commit_ids += run_command(['git', 'rev-parse', '--revs-only', commitish]).splitlines()
-        return commit_ids
-
-    def commit_message_for_local_commit(self, commit_id):
-        commit_lines = run_command(['git', 'cat-file', 'commit', commit_id]).splitlines()
-
-        # Skip the git headers.
-        first_line_after_headers = 0
-        for line in commit_lines:
-            first_line_after_headers += 1
-            if line == "":
-                break
-        return CommitMessage(commit_lines[first_line_after_headers:])
-
-    def files_changed_summary_for_commit(self, commit_id):
-        return run_command(['git', 'diff-tree', '--shortstat', '--no-commit-id', commit_id])
diff --git a/WebKitTools/Scripts/modules/scm_unittest.py b/WebKitTools/Scripts/modules/scm_unittest.py
deleted file mode 100644
index 8e82f3c..0000000
--- a/WebKitTools/Scripts/modules/scm_unittest.py
+++ /dev/null
@@ -1,594 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# Copyright (C) 2009 Apple Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import base64
-import os
-import os.path
-import re
-import stat
-import subprocess
-import tempfile
-import unittest
-import urllib
-
-from datetime import date
-from modules.executive import Executive, run_command, ScriptError
-from modules.scm import detect_scm_system, SCM, CheckoutNeedsUpdate, commit_error_handler
-
-# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
-# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
-
-# FIXME: This should be unified into one of the executive.py commands!
-def run_silent(args, cwd=None):
-    process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
-    process.communicate() # ignore output
-    exit_code = process.wait()
-    if exit_code:
-        raise ScriptError('Failed to run "%s"  exit_code: %d  cwd: %s' % (args, exit_code, cwd))
-
-def write_into_file_at_path(file_path, contents):
-    file = open(file_path, 'w')
-    file.write(contents)
-    file.close()
-
-def read_from_path(file_path):
-    file = open(file_path, 'r')
-    contents = file.read()
-    file.close()
-    return contents
-
-# Exists to share svn repository creation code between the git and svn tests
-class SVNTestRepository:
-    @staticmethod
-    def _setup_test_commits(test_object):
-        # Add some test commits
-        os.chdir(test_object.svn_checkout_path)
-        test_file = open('test_file', 'w')
-        test_file.write("test1")
-        test_file.flush()
-        
-        run_command(['svn', 'add', 'test_file'])
-        run_command(['svn', 'commit', '--quiet', '--message', 'initial commit'])
-        
-        test_file.write("test2")
-        test_file.flush()
-        
-        run_command(['svn', 'commit', '--quiet', '--message', 'second commit'])
-        
-        test_file.write("test3\n")
-        test_file.flush()
-        
-        run_command(['svn', 'commit', '--quiet', '--message', 'third commit'])
-
-        test_file.write("test4\n")
-        test_file.close()
-
-        run_command(['svn', 'commit', '--quiet', '--message', 'fourth commit'])
-
-        # svn does not seem to update after commit as I would expect.
-        run_command(['svn', 'update'])
-
-    @classmethod
-    def setup(cls, test_object):
-        # Create an test SVN repository
-        test_object.svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
-        test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path # Not sure this will work on windows
-        # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
-        # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
-        run_command(['svnadmin', 'create', '--pre-1.5-compatible', test_object.svn_repo_path])
-
-        # Create a test svn checkout
-        test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
-        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
-
-        cls._setup_test_commits(test_object)
-
-    @classmethod
-    def tear_down(cls, test_object):
-        run_command(['rm', '-rf', test_object.svn_repo_path])
-        run_command(['rm', '-rf', test_object.svn_checkout_path])
-
-# For testing the SCM baseclass directly.
-class SCMClassTests(unittest.TestCase):
-    def setUp(self):
-        self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
-
-    def tearDown(self):
-        self.dev_null.close()
-
-    def test_run_command_with_pipe(self):
-        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
-        self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
-
-        # Test the non-pipe case too:
-        self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
-
-        command_returns_non_zero = ['/bin/sh', '--invalid-option']
-        # Test when the input pipe process fails.
-        input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
-        self.assertTrue(input_process.poll() != 0)
-        self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
-
-        # Test when the run_command process fails.
-        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments.
-        self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
-
-    def test_error_handlers(self):
-        git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469"
-        svn_failure_message="""svn: Commit failed (details follow):
-svn: File or directory 'ChangeLog' is out of date; try updating
-svn: resource out of date; try updating
-"""
-        command_does_not_exist = ['does_not_exist', 'invalid_option']
-        self.assertRaises(OSError, run_command, command_does_not_exist)
-        self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
-
-        command_returns_non_zero = ['/bin/sh', '--invalid-option']
-        self.assertRaises(ScriptError, run_command, command_returns_non_zero)
-        # Check if returns error text:
-        self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
-
-        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
-        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
-        self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
-
-
-# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
-class SCMTest(unittest.TestCase):
-    def _create_patch(self, patch_contents):
-        patch_path = os.path.join(self.svn_checkout_path, 'patch.diff')
-        write_into_file_at_path(patch_path, patch_contents)
-        patch = {}
-        patch['reviewer'] = 'Joe Cool'
-        patch['bug_id'] = '12345'
-        patch['url'] = 'file://%s' % urllib.pathname2url(patch_path)
-        return patch
-
-    def _setup_webkittools_scripts_symlink(self, local_scm):
-        webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
-        webkit_scripts_directory = webkit_scm.scripts_directory()
-        local_scripts_directory = local_scm.scripts_directory()
-        os.mkdir(os.path.dirname(local_scripts_directory))
-        os.symlink(webkit_scripts_directory, local_scripts_directory)
-
-    # Tests which both GitTest and SVNTest should run.
-    # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
-    def _shared_test_commit_with_message(self):
-        write_into_file_at_path('test_file', 'more test content')
-        commit_text = self.scm.commit_with_message('another test commit')
-        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '5')
-
-        self.scm.dryrun = True
-        write_into_file_at_path('test_file', 'still more test content')
-        commit_text = self.scm.commit_with_message('yet another test commit')
-        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
-
-    def _shared_test_reverse_diff(self):
-        self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
-        # Only test the simple case, as any other will end up with conflict markers.
-        self.scm.apply_reverse_diff('4')
-        self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
-
-    def _shared_test_diff_for_revision(self):
-        # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
-        r3_patch = self.scm.diff_for_revision(3)
-        self.assertTrue(re.search('test3', r3_patch))
-        self.assertFalse(re.search('test4', r3_patch))
-        self.assertTrue(re.search('test2', r3_patch))
-        self.assertTrue(re.search('test2', self.scm.diff_for_revision(2)))
-
-    def _shared_test_svn_apply_git_patch(self):
-        self._setup_webkittools_scripts_symlink(self.scm)
-        git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
-new file mode 100644
-index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
-60151690
-GIT binary patch
-literal 512
-zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
-zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
-zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
-zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
-zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
-zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
-zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
-z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
-z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
-ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
-
-literal 0
-HcmV?d00001
-
-"""
-        self.scm.apply_patch(self._create_patch(git_binary_addition))
-        added = read_from_path('fizzbuzz7.gif')
-        self.assertEqual(512, len(added))
-        self.assertTrue(added.startswith('GIF89a'))
-        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
-
-        # The file already exists.
-        self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_addition))
-
-        git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
-index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
-GIT binary patch
-literal 7
-OcmYex&reD$;sO8*F9L)B
-
-literal 512
-zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
-zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
-zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
-zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
-zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
-zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
-zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
-z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
-z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
-ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
-
-"""
-        self.scm.apply_patch(self._create_patch(git_binary_modification))
-        modified = read_from_path('fizzbuzz7.gif')
-        self.assertEqual('foobar\n', modified)
-        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
-
-        # Applying the same modification should fail.
-        self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_modification))
-
-        git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
-deleted file mode 100644
-index 323fae0..0000000
-GIT binary patch
-literal 0
-HcmV?d00001
-
-literal 7
-OcmYex&reD$;sO8*F9L)B
-
-"""
-        self.scm.apply_patch(self._create_patch(git_binary_deletion))
-        self.assertFalse(os.path.exists('fizzbuzz7.gif'))
-        self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
-
-        # Cannot delete again.
-        self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_deletion))
-
-
-class SVNTest(SCMTest):
-
-    @staticmethod
-    def _set_date_and_reviewer(changelog_entry):
-        # Joe Cool matches the reviewer set in SCMTest._create_patch
-        changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
-        # svn-apply will update ChangeLog entries with today's date.
-        return changelog_entry.replace('DATE_HERE', date.today().isoformat())
-
-    def test_svn_apply(self):
-        first_entry = """2009-10-26  Eric Seidel  <eric at webkit.org>
-
-        Reviewed by Foo Bar.
-
-        Most awesome change ever.
-
-        * scm_unittest.py:
-"""
-        intermediate_entry = """2009-10-27  Eric Seidel  <eric at webkit.org>
-
-        Reviewed by Baz Bar.
-
-        A more awesomer change yet!
-
-        * scm_unittest.py:
-"""
-        one_line_overlap_patch = """Index: ChangeLog
-===================================================================
---- ChangeLog	(revision 5)
-+++ ChangeLog	(working copy)
-@@ -1,5 +1,13 @@
- 2009-10-26  Eric Seidel  <eric at webkit.org>
-
-+        Reviewed by NOBODY (OOPS!).
-+
-+        Second most awsome change ever.
-+
-+        * scm_unittest.py:
-+
-+2009-10-26  Eric Seidel  <eric at webkit.org>
-+
-         Reviewed by Foo Bar.
-
-         Most awesome change ever.
-"""
-        one_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric at webkit.org>
-
-        Reviewed by REVIEWER_HERE.
-
-        Second most awsome change ever.
-
-        * scm_unittest.py:
-"""
-        two_line_overlap_patch = """Index: ChangeLog
-===================================================================
---- ChangeLog	(revision 5)
-+++ ChangeLog	(working copy)
-@@ -2,6 +2,14 @@
-
-         Reviewed by Foo Bar.
-
-+        Second most awsome change ever.
-+
-+        * scm_unittest.py:
-+
-+2009-10-26  Eric Seidel  <eric at webkit.org>
-+
-+        Reviewed by Foo Bar.
-+
-         Most awesome change ever.
-
-         * scm_unittest.py:
-"""
-        two_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric at webkit.org>
-
-        Reviewed by Foo Bar.
-
-        Second most awsome change ever.
-
-        * scm_unittest.py:
-"""
-        write_into_file_at_path('ChangeLog', first_entry)
-        run_command(['svn', 'add', 'ChangeLog'])
-        run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
-
-        # Patch files were created against just 'first_entry'.
-        # Add a second commit to make svn-apply have to apply the patches with fuzz.
-        changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
-        write_into_file_at_path('ChangeLog', changelog_contents)
-        run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
-
-        self._setup_webkittools_scripts_symlink(self.scm)
-        self.scm.apply_patch(self._create_patch(one_line_overlap_patch))
-        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
-        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
-
-        self.scm.revert_files(['ChangeLog'])
-        self.scm.apply_patch(self._create_patch(two_line_overlap_patch))
-        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
-        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
-
-    def setUp(self):
-        SVNTestRepository.setup(self)
-        os.chdir(self.svn_checkout_path)
-        self.scm = detect_scm_system(self.svn_checkout_path)
-
-    def tearDown(self):
-        SVNTestRepository.tear_down(self)
-
-    def test_create_patch_is_full_patch(self):
-        test_dir_path = os.path.join(self.svn_checkout_path, 'test_dir')
-        os.mkdir(test_dir_path)
-        test_file_path = os.path.join(test_dir_path, 'test_file2')
-        write_into_file_at_path(test_file_path, 'test content')
-        run_command(['svn', 'add', 'test_dir'])
-
-        # create_patch depends on 'svn-create-patch', so make a dummy version.
-        scripts_path = os.path.join(self.svn_checkout_path, 'WebKitTools', 'Scripts')
-        os.makedirs(scripts_path)
-        create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
-        write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n.
-        os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
-
-        # Change into our test directory and run the create_patch command.
-        os.chdir(test_dir_path)
-        scm = detect_scm_system(test_dir_path)
-        self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
-        patch_contents = scm.create_patch()
-        # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
-        self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
-
-    def test_detection(self):
-        scm = detect_scm_system(self.svn_checkout_path)
-        self.assertEqual(scm.display_name(), "svn")
-        self.assertEqual(scm.supports_local_commits(), False)
-
-    def test_apply_small_binary_patch(self):
-        patch_contents = """Index: test_file.swf
-===================================================================
-Cannot display: file marked as a binary type.
-svn:mime-type = application/octet-stream
-
-Property changes on: test_file.swf
-___________________________________________________________________
-Name: svn:mime-type
-   + application/octet-stream
-
-
-Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
-"""
-        expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
-        self._setup_webkittools_scripts_symlink(self.scm)
-        patch_file = self._create_patch(patch_contents)
-        self.scm.apply_patch(patch_file)
-        actual_contents = read_from_path("test_file.swf")
-        self.assertEqual(actual_contents, expected_contents)
-
-    def test_apply_svn_patch(self):
-        scm = detect_scm_system(self.svn_checkout_path)
-        patch = self._create_patch(run_command(['svn', 'diff', '-r4:3']))
-        self._setup_webkittools_scripts_symlink(scm)
-        scm.apply_patch(patch)
-
-    def test_apply_svn_patch_force(self):
-        scm = detect_scm_system(self.svn_checkout_path)
-        patch = self._create_patch(run_command(['svn', 'diff', '-r2:4']))
-        self._setup_webkittools_scripts_symlink(scm)
-        self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
-
-    def test_commit_logs(self):
-        # Commits have dates and usernames in them, so we can't just direct compare.
-        self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
-        self.assertTrue(re.search('second commit', self.scm.svn_commit_log(2)))
-
-    def test_commit_text_parsing(self):
-        self._shared_test_commit_with_message()
-
-    def test_reverse_diff(self):
-        self._shared_test_reverse_diff()
-
-    def test_diff_for_revision(self):
-        self._shared_test_diff_for_revision()
-
-    def test_svn_apply_git_patch(self):
-        self._shared_test_svn_apply_git_patch()
-
-class GitTest(SCMTest):
-
-    def _setup_git_clone_of_svn_repository(self):
-        self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
-        # --quiet doesn't make git svn silent, so we use run_silent to redirect output
-        run_silent(['git', 'svn', '--quiet', 'clone', self.svn_repo_url, self.git_checkout_path])
-
-    def _tear_down_git_clone_of_svn_repository(self):
-        run_command(['rm', '-rf', self.git_checkout_path])
-
-    def setUp(self):
-        SVNTestRepository.setup(self)
-        self._setup_git_clone_of_svn_repository()
-        os.chdir(self.git_checkout_path)
-        self.scm = detect_scm_system(self.git_checkout_path)
-
-    def tearDown(self):
-        SVNTestRepository.tear_down(self)
-        self._tear_down_git_clone_of_svn_repository()
-
-    def test_detection(self):
-        scm = detect_scm_system(self.git_checkout_path)
-        self.assertEqual(scm.display_name(), "git")
-        self.assertEqual(scm.supports_local_commits(), True)
-
-    def test_rebase_in_progress(self):
-        svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
-        write_into_file_at_path(svn_test_file, "svn_checkout")
-        run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
-
-        git_test_file = os.path.join(self.git_checkout_path, 'test_file')
-        write_into_file_at_path(git_test_file, "git_checkout")
-        run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
-
-        # --quiet doesn't make git svn silent, so use run_silent to redirect output
-        self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
-
-        scm = detect_scm_system(self.git_checkout_path)
-        self.assertTrue(scm.rebase_in_progress())
-
-        # Make sure our cleanup works.
-        scm.clean_working_directory()
-        self.assertFalse(scm.rebase_in_progress())
-
-        # Make sure cleanup doesn't throw when no rebase is in progress.
-        scm.clean_working_directory()
-
-    def test_commitish_parsing(self):
-        scm = detect_scm_system(self.git_checkout_path)
-    
-        # Multiple revisions are cherry-picked.
-        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
-        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
-    
-        # ... is an invalid range specifier
-        self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
-
-    def test_commitish_order(self):
-        scm = detect_scm_system(self.git_checkout_path)
-
-        commit_range = 'HEAD~3..HEAD'
-
-        actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
-        expected_commits = []
-        expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
-
-        self.assertEqual(actual_commits, expected_commits)
-
-    def test_apply_git_patch(self):
-        scm = detect_scm_system(self.git_checkout_path)
-        patch = self._create_patch(run_command(['git', 'diff', 'HEAD..HEAD^']))
-        self._setup_webkittools_scripts_symlink(scm)
-        scm.apply_patch(patch)
-
-    def test_apply_git_patch_force(self):
-        scm = detect_scm_system(self.git_checkout_path)
-        patch = self._create_patch(run_command(['git', 'diff', 'HEAD~2..HEAD']))
-        self._setup_webkittools_scripts_symlink(scm)
-        self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
-
-    def test_commit_text_parsing(self):
-        self._shared_test_commit_with_message()
-
-    def test_reverse_diff(self):
-        self._shared_test_reverse_diff()
-
-    def test_diff_for_revision(self):
-        self._shared_test_diff_for_revision()
-
-    def test_svn_apply_git_patch(self):
-        self._shared_test_svn_apply_git_patch()
-
-    def test_create_binary_patch(self):
-        # Create a git binary patch and check the contents.
-        scm = detect_scm_system(self.git_checkout_path)
-        test_file_name = 'binary_file'
-        test_file_path = os.path.join(self.git_checkout_path, test_file_name)
-        file_contents = ''.join(map(chr, range(256)))
-        write_into_file_at_path(test_file_path, file_contents)
-        run_command(['git', 'add', test_file_name])
-        patch = scm.create_patch()
-        self.assertTrue(re.search(r'\nliteral 0\n', patch))
-        self.assertTrue(re.search(r'\nliteral 256\n', patch))
-
-        # Check if we can apply the created patch.
-        run_command(['git', 'rm', '-f', test_file_name])
-        self._setup_webkittools_scripts_symlink(scm)
-        self.scm.apply_patch(self._create_patch(patch))
-        self.assertEqual(file_contents, read_from_path(test_file_path))
-
-        # Check if we can create a patch from a local commit.
-        write_into_file_at_path(test_file_path, file_contents)
-        run_command(['git', 'add', test_file_name])
-        run_command(['git', 'commit', '-m', 'binary diff'])
-        patch_from_local_commit = scm.create_patch_from_local_commit('HEAD')
-        self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
-        self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
-        patch_since_local_commit = scm.create_patch_since_local_commit('HEAD^1')
-        self.assertTrue(re.search(r'\nliteral 0\n', patch_since_local_commit))
-        self.assertTrue(re.search(r'\nliteral 256\n', patch_since_local_commit))
-        self.assertEqual(patch_from_local_commit, patch_since_local_commit)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/statusbot.py b/WebKitTools/Scripts/modules/statusbot.py
deleted file mode 100644
index 2928a4e..0000000
--- a/WebKitTools/Scripts/modules/statusbot.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# WebKit's Python module for interacting with the Commit Queue status page.
-
-from modules.webkit_logging import log
-from modules.webkit_mechanize import Browser
-
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
-# so this import should always succeed.
-from .BeautifulSoup import BeautifulSoup
-
-import urllib2
-
-
-class StatusBot:
-    default_host = "webkit-commit-queue.appspot.com"
-
-    def __init__(self, host=default_host):
-        self.set_host(host)
-        self.browser = Browser()
-
-    def set_host(self, host):
-        self.statusbot_host = host
-        self.statusbot_server_url = "http://%s" % self.statusbot_host
-
-    def results_url_for_status(self, status_id):
-        return "%s/results/%s" % (self.statusbot_server_url, status_id)
-
-    def update_status(self, queue_name, status, patch=None, results_file=None):
-        # During unit testing, statusbot_host is None
-        if not self.statusbot_host:
-            return
-
-        log(status)
-        update_status_url = "%s/update-status" % self.statusbot_server_url
-        self.browser.open(update_status_url)
-        self.browser.select_form(name="update_status")
-        self.browser['queue_name'] = queue_name
-        if patch:
-            if patch.get('bug_id'):
-                self.browser['bug_id'] = str(patch['bug_id'])
-            if patch.get('id'):
-                self.browser['patch_id'] = str(patch['id'])
-        self.browser['status'] = status
-        if results_file:
-            self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file')
-        response = self.browser.submit()
-        return response.read() # This is the id of the newly created status object.
-
-    def patch_status(self, queue_name, patch_id):
-        update_status_url = "%s/patch-status/%s/%s" % (self.statusbot_server_url, queue_name, patch_id)
-        try:
-            return urllib2.urlopen(update_status_url).read()
-        except urllib2.HTTPError, e:
-            if e.code == 404:
-                return None
-            raise e
diff --git a/WebKitTools/Scripts/modules/stepsequence.py b/WebKitTools/Scripts/modules/stepsequence.py
deleted file mode 100644
index 39752c1..0000000
--- a/WebKitTools/Scripts/modules/stepsequence.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from modules.buildsteps import CommandOptions
-from modules.executive import ScriptError
-from modules.webkit_logging import log
-from modules.scm import CheckoutNeedsUpdate
-from modules.queueengine import QueueEngine
-
-
-class StepSequenceErrorHandler():
-    @classmethod
-    def handle_script_error(cls, tool, patch, script_error):
-        raise NotImplementedError, "subclasses must implement"
-
-
-class StepSequence(object):
-    def __init__(self, steps):
-        self._steps = steps or []
-
-    def options(self):
-        collected_options = [
-            CommandOptions.parent_command,
-            CommandOptions.quiet,
-        ]
-        for step in self._steps:
-            collected_options = collected_options + step.options()
-        # Remove duplicates.
-        collected_options = sorted(set(collected_options))
-        return collected_options
-
-    def _run(self, tool, options, state):
-        for step in self._steps:
-            step(tool, options).run(state)
-
-    def run_and_handle_errors(self, tool, options, state=None):
-        if not state:
-            state = {}
-        try:
-            self._run(tool, options, state)
-        except CheckoutNeedsUpdate, e:
-            log("Commit failed because the checkout is out of date.  Please update and try again.")
-            log("You can pass --no-build to skip building/testing after update if you believe the new commits did not affect the results.")
-            QueueEngine.exit_after_handled_error(e)
-        except ScriptError, e:
-            if not options.quiet:
-                log(e.message_with_output())
-            if options.parent_command:
-                command = tool.command_by_name(options.parent_command)
-                command.handle_script_error(tool, state, e)
-            QueueEngine.exit_after_handled_error(e)
diff --git a/WebKitTools/Scripts/modules/webkit_logging_unittest.py b/WebKitTools/Scripts/modules/webkit_logging_unittest.py
deleted file mode 100644
index cbeaa4b..0000000
--- a/WebKitTools/Scripts/modules/webkit_logging_unittest.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#    * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#    * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#    * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import os
-import subprocess
-import StringIO
-import tempfile
-import unittest
-
-from modules.executive import ScriptError
-from modules.webkit_logging import *
-
-class LoggingTest(unittest.TestCase):
-
-    def assert_log_equals(self, log_input, expected_output):
-        original_stderr = sys.stderr
-        test_stderr = StringIO.StringIO()
-        sys.stderr = test_stderr
-
-        try:
-            log(log_input)
-            actual_output = test_stderr.getvalue()
-        finally:
-            original_stderr = original_stderr
-
-        self.assertEquals(actual_output, expected_output, "log(\"%s\") expected: %s actual: %s" % (log_input, expected_output, actual_output))
-
-    def test_log(self):
-        self.assert_log_equals("test", "test\n")
-
-        # Test that log() does not throw an exception when passed an object instead of a string.
-        self.assert_log_equals(ScriptError(message="ScriptError"), "ScriptError\n")
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/modules/webkitport_unittest.py b/WebKitTools/Scripts/modules/webkitport_unittest.py
deleted file mode 100644
index 3430755..0000000
--- a/WebKitTools/Scripts/modules/webkitport_unittest.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import unittest
-
-from modules.webkitport import WebKitPort, MacPort, GtkPort, QtPort, ChromiumPort
-
-class WebKitPortTest(unittest.TestCase):
-    def test_mac_port(self):
-        self.assertEquals(MacPort.name(), "Mac")
-        self.assertEquals(MacPort.flag(), "--port=mac")
-        self.assertEquals(MacPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
-        self.assertEquals(MacPort.build_webkit_command(), [WebKitPort.script_path("build-webkit")])
-        self.assertEquals(MacPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug"])
-        self.assertEquals(MacPort.build_webkit_command(build_style="release"), [WebKitPort.script_path("build-webkit"), "--release"])
-
-    def test_gtk_port(self):
-        self.assertEquals(GtkPort.name(), "Gtk")
-        self.assertEquals(GtkPort.flag(), "--port=gtk")
-        self.assertEquals(GtkPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests"), "--gtk"])
-        self.assertEquals(GtkPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--gtk"])
-        self.assertEquals(GtkPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--gtk"])
-
-    def test_qt_port(self):
-        self.assertEquals(QtPort.name(), "Qt")
-        self.assertEquals(QtPort.flag(), "--port=qt")
-        self.assertEquals(QtPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
-        self.assertEquals(QtPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--qt", '--makeargs="-j8"'])
-        self.assertEquals(QtPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--qt", '--makeargs="-j8"'])
-
-    def test_chromium_port(self):
-        self.assertEquals(ChromiumPort.name(), "Chromium")
-        self.assertEquals(ChromiumPort.flag(), "--port=chromium")
-        self.assertEquals(ChromiumPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
-        self.assertEquals(ChromiumPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--chromium"])
-        self.assertEquals(ChromiumPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--chromium"])
-        self.assertEquals(ChromiumPort.update_webkit_command(), [WebKitPort.script_path("update-webkit"), "--chromium"])
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/WebKitTools/Scripts/test-webkit-python b/WebKitTools/Scripts/test-webkit-python
deleted file mode 100755
index 19284e5..0000000
--- a/WebKitTools/Scripts/test-webkit-python
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-import sys
-import unittest
-
-from modules.bugzilla_unittest import *
-from modules.buildbot_unittest import *
-from modules.buildsteps_unittest import *
-from modules.changelogs_unittest import *
-from modules.commands.download_unittest import *
-from modules.commands.early_warning_system_unittest import *
-from modules.commands.upload_unittest import *
-from modules.commands.queries_unittest import *
-from modules.commands.queues_unittest import *
-from modules.committers_unittest import *
-from modules.credentials_unittest import *
-from modules.cpp_style_unittest import *
-from modules.diff_parser_unittest import *
-from modules.executive_unittest import *
-from modules.multicommandtool_unittest import *
-from modules.queueengine_unittest import *
-from modules.style_unittest import *
-from modules.text_style_unittest import *
-from modules.webkit_logging_unittest import *
-from modules.webkitport_unittest import *
-
-if __name__ == "__main__":
-    # FIXME: This is a hack, but I'm tired of commenting out the test.
-    #        See https://bugs.webkit.org/show_bug.cgi?id=31818
-    if len(sys.argv) > 1 and sys.argv[1] == "--all":
-        sys.argv.remove("--all")
-        from modules.scm_unittest import *
-
-    unittest.main()
diff --git a/WebKitTools/Scripts/test-webkitpy b/WebKitTools/Scripts/test-webkitpy
new file mode 100755
index 0000000..e919c81
--- /dev/null
+++ b/WebKitTools/Scripts/test-webkitpy
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import sys
+import unittest
+
+from webkitpy.bugzilla_unittest import *
+from webkitpy.buildbot_unittest import *
+from webkitpy.buildsteps_unittest import *
+from webkitpy.changelogs_unittest import *
+from webkitpy.commands.download_unittest import *
+from webkitpy.commands.early_warning_system_unittest import *
+from webkitpy.commands.upload_unittest import *
+from webkitpy.commands.queries_unittest import *
+from webkitpy.commands.queues_unittest import *
+from webkitpy.committers_unittest import *
+from webkitpy.credentials_unittest import *
+from webkitpy.cpp_style_unittest import *
+from webkitpy.diff_parser_unittest import *
+from webkitpy.executive_unittest import *
+from webkitpy.multicommandtool_unittest import *
+from webkitpy.queueengine_unittest import *
+from webkitpy.style_unittest import *
+from webkitpy.text_style_unittest import *
+from webkitpy.webkit_logging_unittest import *
+from webkitpy.webkitport_unittest import *
+
+if __name__ == "__main__":
+    # FIXME: This is a hack, but I'm tired of commenting out the test.
+    #        See https://bugs.webkit.org/show_bug.cgi?id=31818
+    if len(sys.argv) > 1 and sys.argv[1] == "--all":
+        sys.argv.remove("--all")
+        from webkitpy.scm_unittest import *
+
+    unittest.main()
diff --git a/WebKitTools/Scripts/validate-committer-lists b/WebKitTools/Scripts/validate-committer-lists
index b41d9bb..e39692e 100755
--- a/WebKitTools/Scripts/validate-committer-lists
+++ b/WebKitTools/Scripts/validate-committer-lists
@@ -36,13 +36,13 @@ import subprocess
 import re
 import urllib2
 from datetime import date, datetime, timedelta
-from modules.committers import CommitterList
-from modules.logging import log, error
-from modules.scm import Git
+from webkitpy.committers import CommitterList
+from webkitpy.logging import log, error
+from webkitpy.scm import Git
 
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
 # so this import should always succeed.
-from modules.BeautifulSoup import BeautifulSoup
+from webkitpy.BeautifulSoup import BeautifulSoup
 
 def print_list_if_non_empty(title, list_to_print):
     if not list_to_print:
diff --git a/WebKitTools/Scripts/modules/BeautifulSoup.py b/WebKitTools/Scripts/webkitpy/BeautifulSoup.py
similarity index 100%
rename from WebKitTools/Scripts/modules/BeautifulSoup.py
rename to WebKitTools/Scripts/webkitpy/BeautifulSoup.py
diff --git a/WebKitTools/Scripts/modules/__init__.py b/WebKitTools/Scripts/webkitpy/__init__.py
similarity index 100%
rename from WebKitTools/Scripts/modules/__init__.py
rename to WebKitTools/Scripts/webkitpy/__init__.py
diff --git a/WebKitTools/Scripts/webkitpy/bugzilla.py b/WebKitTools/Scripts/webkitpy/bugzilla.py
new file mode 100644
index 0000000..4f73d59
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/bugzilla.py
@@ -0,0 +1,563 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# WebKit's Python module for interacting with Bugzilla
+
+import re
+import subprocess
+
+from datetime import datetime # used in timestamp()
+
+# Import WebKit-specific modules.
+from webkitpy.webkit_logging import error, log
+from webkitpy.committers import CommitterList
+from webkitpy.credentials import Credentials
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/modules
+# so this import should always succeed.
+from .BeautifulSoup import BeautifulSoup, SoupStrainer
+
+from webkitpy.webkit_mechanize import Browser
+
+def parse_bug_id(message):
+    match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", message)
+    if match:
+        return int(match.group('bug_id'))
+    match = re.search(Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)", message)
+    if match:
+        return int(match.group('bug_id'))
+    return None
+
+
+def timestamp():
+    return datetime.now().strftime("%Y%m%d%H%M%S")
+
+
+# FIXME: This class is kinda a hack for now.  It exists so we have one place
+# to hold bug logic, even if much of the code deals with dictionaries still.
+class Bug(object):
+    def __init__(self, bug_dictionary):
+        self.bug_dictionary = bug_dictionary
+
+    def assigned_to_email(self):
+        return self.bug_dictionary["assigned_to_email"]
+
+    # Rarely do we actually want obsolete attachments
+    def attachments(self, include_obsolete=False):
+        if include_obsolete:
+            return self.bug_dictionary["attachments"][:] # Return a copy in either case.
+        return [attachment for attachment in self.bug_dictionary["attachments"] if not attachment["is_obsolete"]]
+
+    def patches(self, include_obsolete=False):
+        return [patch for patch in self.attachments(include_obsolete) if patch["is_patch"]]
+
+    def unreviewed_patches(self):
+        return [patch for patch in self.patches() if patch.get("review") == "?"]
+
+
+# A container for all of the logic for making a parsing buzilla queries.
+class BugzillaQueries(object):
+    def __init__(self, bugzilla):
+        self.bugzilla = bugzilla
+
+    def _load_query(self, query):
+        full_url = "%s%s" % (self.bugzilla.bug_server_url, query)
+        return self.bugzilla.browser.open(full_url)
+
+    def _fetch_bug_ids_advanced_query(self, query):
+        soup = BeautifulSoup(self._load_query(query))
+        # The contents of the <a> inside the cells in the first column happen to be the bug id.
+        return [int(bug_link_cell.find("a").string) for bug_link_cell in soup('td', "first-child")]
+
+    def _parse_attachment_ids_request_query(self, page):
+        digits = re.compile("\d+")
+        attachment_href = re.compile("attachment.cgi\?id=\d+&action=review")
+        attachment_links = SoupStrainer("a", href=attachment_href)
+        return [int(digits.search(tag["href"]).group(0)) for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)]
+
+    def _fetch_attachment_ids_request_query(self, query):
+        return self._parse_attachment_ids_request_query(self._load_query(query))
+
+    # List of all r+'d bugs.
+    def fetch_bug_ids_from_pending_commit_list(self):
+        needs_commit_query_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review%2B"
+        return self._fetch_bug_ids_advanced_query(needs_commit_query_url)
+
+    def fetch_patches_from_pending_commit_list(self):
+        # FIXME: This should not have to go through self.bugzilla
+        return sum([self.bugzilla.fetch_reviewed_patches_from_bug(bug_id) for bug_id in self.fetch_bug_ids_from_pending_commit_list()], [])
+
+    def fetch_bug_ids_from_commit_queue(self):
+        commit_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=commit-queue%2B"
+        return self._fetch_bug_ids_advanced_query(commit_queue_url)
+
+    def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
+        # FIXME: Once reject_invalid_patches is moved out of this function this becomes a simple list comprehension using fetch_bug_ids_from_commit_queue.
+        patches_to_land = []
+        for bug_id in self.fetch_bug_ids_from_commit_queue():
+            # FIXME: This should not have to go through self.bugzilla
+            patches = self.bugzilla.fetch_commit_queue_patches_from_bug(bug_id, reject_invalid_patches)
+            patches_to_land += patches
+        return patches_to_land
+
+    def _fetch_bug_ids_from_review_queue(self):
+        review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?"
+        return self._fetch_bug_ids_advanced_query(review_queue_url)
+
+    def fetch_patches_from_review_queue(self, limit=None):
+        # FIXME: We should probably have a self.fetch_bug to minimize the number of self.bugzilla calls.
+        return sum([self.bugzilla.fetch_bug(bug_id).unreviewed_patches() for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) # [:None] returns the whole array.
+
+    # FIXME: Why do we have both fetch_patches_from_review_queue and fetch_attachment_ids_from_review_queue??
+    # NOTE: This is also the only client of _fetch_attachment_ids_request_query
+    def fetch_attachment_ids_from_review_queue(self):
+        review_queue_url = "request.cgi?action=queue&type=review&group=type"
+        return self._fetch_attachment_ids_request_query(review_queue_url)
+
+
+class Bugzilla(object):
+    def __init__(self, dryrun=False, committers=CommitterList()):
+        self.dryrun = dryrun
+        self.authenticated = False
+        self.queries = BugzillaQueries(self)
+
+        # FIXME: We should use some sort of Browser mock object when in dryrun mode (to prevent any mistakes).
+        self.browser = Browser()
+        # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script
+        self.browser.set_handle_robots(False)
+        self.committers = committers
+
+    # FIXME: Much of this should go into some sort of config module:
+    bug_server_host = "bugs.webkit.org"
+    bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host)
+    bug_server_url = "https://%s/" % bug_server_host
+    unassigned_email = "webkit-unassigned at lists.webkit.org"
+
+    def bug_url_for_bug_id(self, bug_id, xml=False):
+        content_type = "&ctype=xml" if xml else ""
+        return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url, bug_id, content_type)
+
+    def short_bug_url_for_bug_id(self, bug_id):
+        return "http://webkit.org/b/%s" % bug_id
+
+    def attachment_url_for_id(self, attachment_id, action="view"):
+        action_param = ""
+        if action and action != "view":
+            action_param = "&action=%s" % action
+        return "%sattachment.cgi?id=%s%s" % (self.bug_server_url, attachment_id, action_param)
+
+    def _parse_attachment_flag(self, element, flag_name, attachment, result_key):
+        flag = element.find('flag', attrs={'name' : flag_name})
+        if flag:
+            attachment[flag_name] = flag['status']
+            if flag['status'] == '+':
+                attachment[result_key] = flag['setter']
+
+    def _parse_attachment_element(self, element, bug_id):
+        attachment = {}
+        attachment['bug_id'] = bug_id
+        attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
+        attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
+        attachment['id'] = int(element.find('attachid').string)
+        attachment['url'] = self.attachment_url_for_id(attachment['id'])
+        attachment['name'] = unicode(element.find('desc').string)
+        attachment['attacher_email'] = str(element.find('attacher').string)
+        attachment['type'] = str(element.find('type').string)
+        self._parse_attachment_flag(element, 'review', attachment, 'reviewer_email')
+        self._parse_attachment_flag(element, 'commit-queue', attachment, 'committer_email')
+        return attachment
+
+    def _parse_bug_page(self, page):
+        soup = BeautifulSoup(page)
+        bug = {}
+        bug["id"] = int(soup.find("bug_id").string)
+        bug["title"] = unicode(soup.find("short_desc").string)
+        bug["reporter_email"] = str(soup.find("reporter").string)
+        bug["assigned_to_email"] = str(soup.find("assigned_to").string)
+        bug["cc_emails"] = [str(element.string) for element in soup.findAll('cc')]
+        bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
+        return bug
+
+    # Makes testing fetch_*_from_bug() possible until we have a better BugzillaNetwork abstration.
+    def _fetch_bug_page(self, bug_id):
+        bug_url = self.bug_url_for_bug_id(bug_id, xml=True)
+        log("Fetching: %s" % bug_url)
+        return self.browser.open(bug_url)
+
+    def fetch_bug_dictionary(self, bug_id):
+        return self._parse_bug_page(self._fetch_bug_page(bug_id))
+
+    # FIXME: A BugzillaCache object should provide all these fetch_ methods.
+    def fetch_bug(self, bug_id):
+        return Bug(self.fetch_bug_dictionary(bug_id))
+
+    def _parse_bug_id_from_attachment_page(self, page):
+        up_link = BeautifulSoup(page).find('link', rel='Up') # The "Up" relation happens to point to the bug.
+        if not up_link:
+            return None # This attachment does not exist (or you don't have permissions to view it).
+        match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href'])
+        return int(match.group('bug_id'))
+
+    def bug_id_for_attachment_id(self, attachment_id):
+        attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
+        log("Fetching: %s" % attachment_url)
+        page = self.browser.open(attachment_url)
+        return self._parse_bug_id_from_attachment_page(page)
+
+    # This should really return an Attachment object
+    # which can lazily fetch any missing data.
+    def fetch_attachment(self, attachment_id):
+        # We could grab all the attachment details off of the attachment edit page
+        # but we already have working code to do so off of the bugs page, so re-use that.
+        bug_id = self.bug_id_for_attachment_id(attachment_id)
+        if not bug_id:
+            return None
+        for attachment in self.fetch_bug(bug_id).attachments(include_obsolete=True):
+            # FIXME: Once we have a real Attachment class we shouldn't paper over this possible comparison failure
+            # and we should remove the int() == int() hacks and leave it just ==.
+            if int(attachment['id']) == int(attachment_id):
+                self._validate_committer_and_reviewer(attachment)
+                return attachment
+        return None # This should never be hit.
+
+    # fetch_patches_from_bug exists until we expose a Bug class outside of bugzilla.py
+    def fetch_patches_from_bug(self, bug_id):
+        return self.fetch_bug(bug_id).patches()
+
+    # _view_source_link belongs in some sort of webkit_config.py module.
+    def _view_source_link(self, local_path):
+        return "http://trac.webkit.org/browser/trunk/%s" % local_path
+
+    def _flag_permission_rejection_message(self, setter_email, flag_name):
+        committer_list = "WebKitTools/Scripts/webkitpy/committers.py" # This could be computed from CommitterList.__file__
+        contribution_guidlines_url = "http://webkit.org/coding/contributing.html" # Should come from some webkit_config.py
+        queue_administrator = "eseidel at chromium.org" # This could be queried from the status_bot.
+        queue_name = "commit-queue" # This could be queried from the tool.
+        rejection_message = "%s does not have %s permissions according to %s." % (setter_email, flag_name, self._view_source_link(committer_list))
+        rejection_message += "\n\n- If you do not have %s rights please read %s for instructions on how to use bugzilla flags." % (flag_name, contribution_guidlines_url)
+        rejection_message += "\n\n- If you have %s rights please correct the error in %s by adding yourself to the file (no review needed)." % (flag_name, committer_list)
+        rejection_message += "  Due to bug 30084 the %s will require a restart after your change." % queue_name
+        rejection_message += "  Please contact %s to request a %s restart." % (queue_administrator, queue_name)
+        rejection_message += "  After restart the %s will correctly respect your %s rights." % (queue_name, flag_name)
+        return rejection_message
+
+    def _validate_setter_email(self, patch, result_key, lookup_function, rejection_function, reject_invalid_patches):
+        setter_email = patch.get(result_key + '_email')
+        if not setter_email:
+            return None
+
+        committer = lookup_function(setter_email)
+        if committer:
+            patch[result_key] = committer.full_name
+            return patch[result_key]
+
+        if reject_invalid_patches:
+            rejection_function(patch['id'], self._flag_permission_rejection_message(setter_email, result_key))
+        else:
+            log("Warning, attachment %s on bug %s has invalid %s (%s)" % (patch['id'], patch['bug_id'], result_key, setter_email))
+        return None
+
+    def _validate_reviewer(self, patch, reject_invalid_patches):
+        return self._validate_setter_email(patch, 'reviewer', self.committers.reviewer_by_email, self.reject_patch_from_review_queue, reject_invalid_patches)
+
+    def _validate_committer(self, patch, reject_invalid_patches):
+        return self._validate_setter_email(patch, 'committer', self.committers.committer_by_email, self.reject_patch_from_commit_queue, reject_invalid_patches)
+
+    # FIXME: This is a hack until we have a real Attachment object.
+    # _validate_committer and _validate_reviewer fill in the 'reviewer' and 'committer'
+    # keys which other parts of the code expect to be filled in.
+    def _validate_committer_and_reviewer(self, patch):
+        self._validate_reviewer(patch, reject_invalid_patches=False)
+        self._validate_committer(patch, reject_invalid_patches=False)
+
+    # FIXME: fetch_reviewed_patches_from_bug and fetch_commit_queue_patches_from_bug
+    # should share more code and use list comprehensions.
+    def fetch_reviewed_patches_from_bug(self, bug_id, reject_invalid_patches=False):
+        reviewed_patches = []
+        for attachment in self.fetch_bug(bug_id).attachments():
+            if self._validate_reviewer(attachment, reject_invalid_patches):
+                reviewed_patches.append(attachment)
+        return reviewed_patches
+
+    def fetch_commit_queue_patches_from_bug(self, bug_id, reject_invalid_patches=False):
+        commit_queue_patches = []
+        for attachment in self.fetch_reviewed_patches_from_bug(bug_id, reject_invalid_patches):
+            if self._validate_committer(attachment, reject_invalid_patches):
+                commit_queue_patches.append(attachment)
+        return commit_queue_patches
+
+    def authenticate(self):
+        if self.authenticated:
+            return
+
+        if self.dryrun:
+            log("Skipping log in for dry run...")
+            self.authenticated = True
+            return
+
+        (username, password) = Credentials(self.bug_server_host, git_prefix="bugzilla").read_credentials()
+
+        log("Logging in as %s..." % username)
+        self.browser.open(self.bug_server_url + "index.cgi?GoAheadAndLogIn=1")
+        self.browser.select_form(name="login")
+        self.browser['Bugzilla_login'] = username
+        self.browser['Bugzilla_password'] = password
+        response = self.browser.submit()
+
+        match = re.search("<title>(.+?)</title>", response.read())
+        # If the resulting page has a title, and it contains the word "invalid" assume it's the login failure page.
+        if match and re.search("Invalid", match.group(1), re.IGNORECASE):
+            # FIXME: We could add the ability to try again on failure.
+            raise Exception("Bugzilla login failed: %s" % match.group(1))
+
+        self.authenticated = True
+
+    def _fill_attachment_form(self, description, patch_file_object, comment_text=None, mark_for_review=False, mark_for_commit_queue=False, bug_id=None):
+        self.browser['description'] = description
+        self.browser['ispatch'] = ("1",)
+        self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',)
+        self.browser['flag_type-3'] = ('?',) if mark_for_commit_queue else ('X',)
+        if bug_id:
+            patch_name = "bug-%s-%s.patch" % (bug_id, timestamp())
+        else:
+            patch_name ="%s.patch" % timestamp()
+        self.browser.add_file(patch_file_object, "text/plain", patch_name, 'data')
+
+    def add_patch_to_bug(self, bug_id, patch_file_object, description, comment_text=None, mark_for_review=False, mark_for_commit_queue=False):
+        self.authenticate()
+        
+        log('Adding patch "%s" to bug %s' % (description, bug_id))
+        if self.dryrun:
+            log(comment_text)
+            return
+
+        self.browser.open("%sattachment.cgi?action=enter&bugid=%s" % (self.bug_server_url, bug_id))
+        self.browser.select_form(name="entryform")
+        self._fill_attachment_form(description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue, bug_id=bug_id)
+        if comment_text:
+            log(comment_text)
+            self.browser['comment'] = comment_text
+        self.browser.submit()
+
+    def prompt_for_component(self, components):
+        log("Please pick a component:")
+        i = 0
+        for name in components:
+            i += 1
+            log("%2d. %s" % (i, name))
+        result = int(raw_input("Enter a number: ")) - 1
+        return components[result]
+
+    def _check_create_bug_response(self, response_html):
+        match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>", response_html)
+        if match:
+            return match.group('bug_id')
+
+        match = re.search('<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">', response_html, re.DOTALL)
+        error_message = "FAIL"
+        if match:
+            text_lines = BeautifulSoup(match.group('error_message')).findAll(text=True)
+            error_message = "\n" + '\n'.join(["  " + line.strip() for line in text_lines if line.strip()])
+        raise Exception("Bug not created: %s" % error_message)
+
+    def create_bug(self, bug_title, bug_description, component=None, patch_file_object=None, patch_description=None, cc=None, mark_for_review=False, mark_for_commit_queue=False):
+        self.authenticate()
+
+        log('Creating bug with title "%s"' % bug_title)
+        if self.dryrun:
+            log(bug_description)
+            return
+
+        self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit")
+        self.browser.select_form(name="Create")
+        component_items = self.browser.find_control('component').items
+        component_names = map(lambda item: item.name, component_items)
+        if not component or component not in component_names:
+            component = self.prompt_for_component(component_names)
+        self.browser['component'] = [component]
+        if cc:
+            self.browser['cc'] = cc
+        self.browser['short_desc'] = bug_title
+        self.browser['comment'] = bug_description
+
+        if patch_file_object:
+            self._fill_attachment_form(patch_description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue)
+
+        response = self.browser.submit()
+
+        bug_id = self._check_create_bug_response(response.read())
+        log("Bug %s created." % bug_id)
+        log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id))
+        return bug_id
+
+    def _find_select_element_for_flag(self, flag_name):
+        # FIXME: This will break if we ever re-order attachment flags
+        if flag_name == "review":
+            return self.browser.find_control(type='select', nr=0)
+        if flag_name == "commit-queue":
+            return self.browser.find_control(type='select', nr=1)
+        raise Exception("Don't know how to find flag named \"%s\"" % flag_name)
+
+    def clear_attachment_flags(self, attachment_id, additional_comment_text=None):
+        self.authenticate()
+
+        comment_text = "Clearing flags on attachment: %s" % attachment_id
+        if additional_comment_text:
+            comment_text += "\n\n%s" % additional_comment_text
+        log(comment_text)
+
+        if self.dryrun:
+            return
+
+        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+        self.browser.select_form(nr=1)
+        self.browser.set_value(comment_text, name='comment', nr=0)
+        self._find_select_element_for_flag('review').value = ("X",)
+        self._find_select_element_for_flag('commit-queue').value = ("X",)
+        self.browser.submit()
+
+    # FIXME: We need a way to test this on a live bugzilla instance.
+    def _set_flag_on_attachment(self, attachment_id, flag_name, flag_value, comment_text, additional_comment_text):
+        self.authenticate()
+
+        if additional_comment_text:
+            comment_text += "\n\n%s" % additional_comment_text
+        log(comment_text)
+
+        if self.dryrun:
+            return
+
+        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+        self.browser.select_form(nr=1)
+        self.browser.set_value(comment_text, name='comment', nr=0)
+        self._find_select_element_for_flag(flag_name).value = (flag_value,)
+        self.browser.submit()
+
+    def reject_patch_from_commit_queue(self, attachment_id, additional_comment_text=None):
+        comment_text = "Rejecting patch %s from commit-queue." % attachment_id
+        self._set_flag_on_attachment(attachment_id, 'commit-queue', '-', comment_text, additional_comment_text)
+
+    def reject_patch_from_review_queue(self, attachment_id, additional_comment_text=None):
+        comment_text = "Rejecting patch %s from review queue." % attachment_id
+        self._set_flag_on_attachment(attachment_id, 'review', '-', comment_text, additional_comment_text)
+
+    # FIXME: All of these bug editing methods have a ridiculous amount of copy/paste code.
+    def obsolete_attachment(self, attachment_id, comment_text = None):
+        self.authenticate()
+
+        log("Obsoleting attachment: %s" % attachment_id)
+        if self.dryrun:
+            log(comment_text)
+            return
+
+        self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+        self.browser.select_form(nr=1)
+        self.browser.find_control('isobsolete').items[0].selected = True
+        # Also clear any review flag (to remove it from review/commit queues)
+        self._find_select_element_for_flag('review').value = ("X",)
+        self._find_select_element_for_flag('commit-queue').value = ("X",)
+        if comment_text:
+            log(comment_text)
+            # Bugzilla has two textareas named 'comment', one is somehow hidden.  We want the first.
+            self.browser.set_value(comment_text, name='comment', nr=0)
+        self.browser.submit()
+
+    def add_cc_to_bug(self, bug_id, email_address_list):
+        self.authenticate()
+
+        log("Adding %s to the CC list for bug %s" % (email_address_list, bug_id))
+        if self.dryrun:
+            return
+
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        self.browser["newcc"] = ", ".join(email_address_list)
+        self.browser.submit()
+
+    def post_comment_to_bug(self, bug_id, comment_text, cc=None):
+        self.authenticate()
+
+        log("Adding comment to bug %s" % bug_id)
+        if self.dryrun:
+            log(comment_text)
+            return
+
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        self.browser["comment"] = comment_text
+        if cc:
+            self.browser["newcc"] = ", ".join(cc)
+        self.browser.submit()
+
+    def close_bug_as_fixed(self, bug_id, comment_text=None):
+        self.authenticate()
+
+        log("Closing bug %s as fixed" % bug_id)
+        if self.dryrun:
+            log(comment_text)
+            return
+
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        if comment_text:
+            log(comment_text)
+            self.browser['comment'] = comment_text
+        self.browser['bug_status'] = ['RESOLVED']
+        self.browser['resolution'] = ['FIXED']
+        self.browser.submit()
+
+    def reassign_bug(self, bug_id, assignee, comment_text=None):
+        self.authenticate()
+
+        log("Assigning bug %s to %s" % (bug_id, assignee))
+        if self.dryrun:
+            log(comment_text)
+            return
+
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        if comment_text:
+            log(comment_text)
+            self.browser["comment"] = comment_text
+        self.browser["assigned_to"] = assignee
+        self.browser.submit()
+
+    def reopen_bug(self, bug_id, comment_text):
+        self.authenticate()
+
+        log("Re-opening bug %s" % bug_id)
+        log(comment_text) # Bugzilla requires a comment when re-opening a bug, so we know it will never be None.
+        if self.dryrun:
+            return
+
+        self.browser.open(self.bug_url_for_bug_id(bug_id))
+        self.browser.select_form(name="changeform")
+        self.browser['bug_status'] = ['REOPENED']
+        self.browser['comment'] = comment_text
+        self.browser.submit()
diff --git a/WebKitTools/Scripts/webkitpy/bugzilla_unittest.py b/WebKitTools/Scripts/webkitpy/bugzilla_unittest.py
new file mode 100644
index 0000000..93a35f9
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/bugzilla_unittest.py
@@ -0,0 +1,302 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.committers import CommitterList, Reviewer, Committer
+from webkitpy.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id
+from webkitpy.outputcapture import OutputCapture
+from webkitpy.mock import Mock
+
+from webkitpy.BeautifulSoup import BeautifulSoup
+
+
+class MockBrowser(object):
+    def open(self, url):
+        pass
+
+    def select_form(self, name):
+        pass
+
+    def __setitem__(self, key, value):
+        pass
+
+    def submit(self):
+        pass
+
+
+class BugzillaTest(unittest.TestCase):
+    _example_attachment = '''
+        <attachment
+          isobsolete="1"
+          ispatch="1"
+          isprivate="0"
+        >
+        <attachid>33721</attachid>
+        <date>2009-07-29 10:23 PDT</date>
+        <desc>Fixed whitespace issue</desc>
+        <filename>patch</filename>
+        <type>text/plain</type>
+        <size>9719</size>
+        <attacher>christian.plesner.hansen at gmail.com</attacher>
+          <flag name="review"
+                id="17931"
+                status="+"
+                setter="one at test.com"
+           />
+          <flag name="commit-queue"
+                id="17932"
+                status="+"
+                setter="two at test.com"
+           />
+        </attachment>
+'''
+    _expected_example_attachment_parsing = {
+        'bug_id' : 100,
+        'is_obsolete' : True,
+        'is_patch' : True,
+        'id' : 33721,
+        'url' : "https://bugs.webkit.org/attachment.cgi?id=33721",
+        'name' : "Fixed whitespace issue",
+        'type' : "text/plain",
+        'review' : '+',
+        'reviewer_email' : 'one at test.com',
+        'commit-queue' : '+',
+        'committer_email' : 'two at test.com',
+        'attacher_email' : 'christian.plesner.hansen at gmail.com',
+    }
+
+    def test_parse_bug_id(self):
+        # FIXME: These would be all better as doctests
+        bugs = Bugzilla()
+        self.assertEquals(12345, parse_bug_id("http://webkit.org/b/12345"))
+        self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345"))
+        self.assertEquals(12345, parse_bug_id(bugs.short_bug_url_for_bug_id(12345)))
+        self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345)))
+        self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345, xml=True)))
+
+        # Our bug parser is super-fragile, but at least we're testing it.
+        self.assertEquals(None, parse_bug_id("http://www.webkit.org/b/12345"))
+        self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&id=12345"))
+
+    _example_bug = """
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
+<bugzilla version="3.2.3"
+          urlbase="https://bugs.webkit.org/"
+          maintainer="admin at webkit.org"
+          exporter="eric at webkit.org"
+>
+    <bug>
+          <bug_id>32585</bug_id>
+          <creation_ts>2009-12-15 15:17 PST</creation_ts>
+          <short_desc>bug to test bugzilla-tool and commit-queue failures</short_desc>
+          <delta_ts>2009-12-27 21:04:50 PST</delta_ts>
+          <reporter_accessible>1</reporter_accessible>
+          <cclist_accessible>1</cclist_accessible>
+          <classification_id>1</classification_id>
+          <classification>Unclassified</classification>
+          <product>WebKit</product>
+          <component>Tools / Tests</component>
+          <version>528+ (Nightly build)</version>
+          <rep_platform>PC</rep_platform>
+          <op_sys>Mac OS X 10.5</op_sys>
+          <bug_status>NEW</bug_status>
+          <priority>P2</priority>
+          <bug_severity>Normal</bug_severity>
+          <target_milestone>---</target_milestone>
+          <everconfirmed>1</everconfirmed>
+          <reporter name="Eric Seidel">eric at webkit.org</reporter>
+          <assigned_to name="Nobody">webkit-unassigned at lists.webkit.org</assigned_to>
+          <cc>foo at bar.com</cc>
+    <cc>example at example.com</cc>
+          <long_desc isprivate="0">
+            <who name="Eric Seidel">eric at webkit.org</who>
+            <bug_when>2009-12-15 15:17:28 PST</bug_when>
+            <thetext>bug to test bugzilla-tool and commit-queue failures
+
+Ignore this bug.  Just for testing failure modes of bugzilla-tool and the commit-queue.</thetext>
+          </long_desc>
+          <attachment 
+              isobsolete="0"
+              ispatch="1"
+              isprivate="0"
+          > 
+            <attachid>45548</attachid> 
+            <date>2009-12-27 23:51 PST</date> 
+            <desc>Patch</desc> 
+            <filename>bug-32585-20091228005112.patch</filename> 
+            <type>text/plain</type> 
+            <size>10882</size> 
+            <attacher>mjs at apple.com</attacher> 
+            
+              <token>1261988248-dc51409e9c421a4358f365fa8bec8357</token> 
+              <data encoding="base64">SW5kZXg6IFdlYktpdC9tYWMvQ2hhbmdlTG9nCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09
+removed-because-it-was-really-long
+ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
+</data>        
+ 
+              <flag name="review"
+                    id="27602"
+                    status="?"
+                    setter="mjs at apple.com"
+               /> 
+          </attachment> 
+    </bug>
+</bugzilla>
+"""
+    _expected_example_bug_parsing = {
+        "id" : 32585,
+        "title" : u"bug to test bugzilla-tool and commit-queue failures",
+        "cc_emails" : ["foo at bar.com", "example at example.com"],
+        "reporter_email" : "eric at webkit.org",
+        "assigned_to_email" : "webkit-unassigned at lists.webkit.org",
+        "attachments" : [{
+            'name': u'Patch',
+            'url': 'https://bugs.webkit.org/attachment.cgi?id=45548',
+            'is_obsolete': False,
+            'review': '?',
+            'is_patch': True,
+            'attacher_email': 'mjs at apple.com',
+            'bug_id': 32585,
+            'type': 'text/plain',
+            'id': 45548
+        }],
+    }
+
+    def _assert_dictionaries_equal(self, actual, expected):
+        # Make sure we aren't parsing more or less than we expect
+        self.assertEquals(sorted(actual.keys()), sorted(expected.keys()))
+
+        for key, expected_value in expected.items():
+            self.assertEquals(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))
+
+    def test_bug_parsing(self):
+        bug = Bugzilla()._parse_bug_page(self._example_bug)
+        self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
+
+    # This could be combined into test_bug_parsing later if desired.
+    def test_attachment_parsing(self):
+        bugzilla = Bugzilla()
+        soup = BeautifulSoup(self._example_attachment)
+        attachment_element = soup.find("attachment")
+        attachment = bugzilla._parse_attachment_element(attachment_element, self._expected_example_attachment_parsing['bug_id'])
+        self.assertTrue(attachment)
+        self._assert_dictionaries_equal(attachment, self._expected_example_attachment_parsing)
+
+    _sample_attachment_detail_page = """
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+                      "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <title>
+  Attachment 41073 Details for Bug 27314</title>
+<link rel="Top" href="https://bugs.webkit.org/">
+    <link rel="Up" href="show_bug.cgi?id=27314">
+"""
+
+    def test_attachment_detail_bug_parsing(self):
+        bugzilla = Bugzilla()
+        self.assertEquals(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
+
+    def test_add_cc_to_bug(self):
+        bugzilla = Bugzilla()
+        bugzilla.browser = MockBrowser()
+        bugzilla.authenticate = lambda: None
+        expected_stderr = "Adding ['adam at example.com'] to the CC list for bug 42\n"
+        OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam at example.com"]], expected_stderr=expected_stderr)
+
+    def test_flag_permission_rejection_message(self):
+        bugzilla = Bugzilla()
+        expected_messsage="""foo at foo.com does not have review permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/committers.py.
+
+- If you do not have review rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have review rights please correct the error in WebKitTools/Scripts/webkitpy/committers.py by adding yourself to the file (no review needed).  Due to bug 30084 the commit-queue will require a restart after your change.  Please contact eseidel at chromium.org to request a commit-queue restart.  After restart the commit-queue will correctly respect your review rights."""
+        self.assertEqual(bugzilla._flag_permission_rejection_message("foo at foo.com", "review"), expected_messsage)
+
+
+class BugzillaQueriesTest(unittest.TestCase):
+    _sample_request_page = """
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+                      "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <title>Request Queue</title>
+  </head>
+<body>
+
+<h3>Flag: review</h3>
+  <table class="requests" cellspacing="0" cellpadding="4" border="1">
+    <tr>
+        <th>Requester</th>
+        <th>Requestee</th>
+        <th>Bug</th>
+        <th>Attachment</th>
+        <th>Created</th>
+    </tr>
+    <tr>
+        <td>Shinichiro Hamaji &lt;hamaji&#64;chromium.org&gt;</td>
+        <td></td>
+        <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
+        <td><a href="attachment.cgi?id=40511&amp;action=review">
+40511: Patch v0</a></td>
+        <td>2009-10-02 04:58 PST</td>
+    </tr>
+    <tr>
+        <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
+        <td></td>
+        <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
+        <td><a href="attachment.cgi?id=40722&amp;action=review">
+40722: Media controls, the simple approach</a></td>
+        <td>2009-10-06 09:13 PST</td>
+    </tr>
+    <tr>
+        <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
+        <td></td>
+        <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
+        <td><a href="attachment.cgi?id=40723&amp;action=review">
+40723: Adjust the media slider thumb size</a></td>
+        <td>2009-10-06 09:15 PST</td>
+    </tr>
+  </table>
+</body>
+</html>
+"""
+
+    def test_request_page_parsing(self):
+        queries = BugzillaQueries(None)
+        self.assertEquals([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
+
+    def test_load_query(self):
+        queries = BugzillaQueries(Mock())
+        queries._load_query("request.cgi?action=queue&type=review&group=type")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/buildbot.py b/WebKitTools/Scripts/webkitpy/buildbot.py
new file mode 100644
index 0000000..b0eb0a4
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildbot.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# WebKit's Python module for interacting with WebKit's buildbot
+
+import re
+import urllib2
+
+# Import WebKit-specific modules.
+from webkitpy.webkit_logging import log
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
+# so this import should always succeed.
+from .BeautifulSoup import BeautifulSoup
+
+class BuildBot:
+    default_host = "build.webkit.org"
+    def __init__(self, host=default_host):
+        self.buildbot_host = host
+        self.buildbot_server_url = "http://%s/" % self.buildbot_host
+        
+        # If any of the Leopard build/test bots or the Windows builders are red we should not be landing patches.
+        # Other builders should be added to this list once they're known to be stable.
+        self.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
+
+    # If WebKit's buildbot has an XMLRPC interface we could use, we could do something more sophisticated here.
+    # For now we just parse out the basics, enough to support basic questions like "is the tree green?"
+    def _parse_builder_status_from_row(self, status_row):
+        status_cells = status_row.findAll('td')
+        builder = {}
+
+        name_link = status_cells[0].find('a')
+        builder['name'] = name_link.string
+        # We could generate the builder_url from the name in a future version of this code.
+        builder['builder_url'] = self.buildbot_server_url + name_link['href']
+
+        status_link = status_cells[1].find('a')
+        if not status_link:
+            # We failed to find a link in the first cell, just give up.
+            # This can happen if a builder is just-added, the first cell will just be "no build"
+            builder['is_green'] = False # Other parts of the code depend on is_green being present.
+            return builder
+        revision_string = status_link.string # Will be either a revision number or a build number
+        # If revision_string has non-digits assume it's not a revision number.
+        builder['built_revision'] = int(revision_string) if not re.match('\D', revision_string) else None
+        builder['is_green'] = not re.search('fail', status_cells[1].renderContents())
+        # We could parse out the build number instead, but for now just store the URL.
+        builder['build_url'] = self.buildbot_server_url + status_link['href']
+
+        # We could parse out the current activity too.
+
+        return builder
+
+    def _builder_statuses_with_names_matching_regexps(self, builder_statuses, name_regexps):
+        builders = []
+        for builder in builder_statuses:
+            for name_regexp in name_regexps:
+                if re.match(name_regexp, builder['name']):
+                    builders.append(builder)
+        return builders
+
+    def red_core_builders(self):
+        red_builders = []
+        for builder in self._builder_statuses_with_names_matching_regexps(self.builder_statuses(), self.core_builder_names_regexps):
+            if not builder['is_green']:
+                red_builders.append(builder)
+        return red_builders
+
+    def red_core_builders_names(self):
+        red_builders = self.red_core_builders()
+        return map(lambda builder: builder['name'], red_builders)
+
+    def core_builders_are_green(self):
+        return not self.red_core_builders()
+
+    def builder_statuses(self):
+        build_status_url = self.buildbot_server_url + 'one_box_per_builder'
+        page = urllib2.urlopen(build_status_url)
+        soup = BeautifulSoup(page)
+
+        builders = []
+        status_table = soup.find('table')
+        for status_row in status_table.findAll('tr'):
+            builder = self._parse_builder_status_from_row(status_row)
+            builders.append(builder)
+        return builders
diff --git a/WebKitTools/Scripts/webkitpy/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/buildbot_unittest.py
new file mode 100644
index 0000000..a42ea85
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildbot_unittest.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.buildbot import BuildBot
+
+from webkitpy.BeautifulSoup import BeautifulSoup
+
+class BuildBotTest(unittest.TestCase):
+
+    _example_one_box_status = '''
+    <table>
+    <tr>
+    <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td>
+      <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td>
+      <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td>
+    <tr>
+    <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td>
+      <td class="LastBuild box" >no build</td>
+      <td align="center" class="Activity building">building<br />< 1 min</td>
+    <tr>
+    <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td>
+      <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td>
+      <td align="center" class="Activity idle">idle</td>
+    </table>
+'''
+    _expected_example_one_box_parsings = [
+        {
+            'builder_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29',
+            'build_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29/builds/3693',
+            'is_green': True,
+            'name': u'Windows Debug (Tests)',
+            'built_revision': 47380
+        },
+        {
+            'builder_url': u'http://build.webkit.org/builders/SnowLeopard%20Intel%20Release',
+            'is_green': False,
+            'name': u'SnowLeopard Intel Release',
+        },
+        {
+            'builder_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release',
+            'build_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release/builds/654',
+            'is_green': False,
+            'name': u'Qt Linux Release',
+            'built_revision': 47383
+        },
+    ]
+
+    def test_status_parsing(self):
+        buildbot = BuildBot()
+
+        soup = BeautifulSoup(self._example_one_box_status)
+        status_table = soup.find("table")
+        input_rows = status_table.findAll('tr')
+
+        for x in range(len(input_rows)):
+            status_row = input_rows[x]
+            expected_parsing = self._expected_example_one_box_parsings[x]
+
+            builder = buildbot._parse_builder_status_from_row(status_row)
+
+            # Make sure we aren't parsing more or less than we expect
+            self.assertEquals(builder.keys(), expected_parsing.keys())
+
+            for key, expected_value in expected_parsing.items():
+                self.assertEquals(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value)))
+
+    def test_core_builder_methods(self):
+        buildbot = BuildBot()
+
+        # Override builder_statuses function to not touch the network.
+        def example_builder_statuses(): # We could use instancemethod() to bind 'self' but we don't need to.
+            return BuildBotTest._expected_example_one_box_parsings
+        buildbot.builder_statuses = example_builder_statuses
+
+        buildbot.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
+        self.assertEquals(buildbot.red_core_builders_names(), [])
+        self.assertTrue(buildbot.core_builders_are_green())
+
+        buildbot.core_builder_names_regexps = [ 'SnowLeopard', 'Qt' ]
+        self.assertEquals(buildbot.red_core_builders_names(), [ u'SnowLeopard Intel Release', u'Qt Linux Release' ])
+        self.assertFalse(buildbot.core_builders_are_green())
+
+    def test_builder_name_regexps(self):
+        buildbot = BuildBot()
+
+        example_builders = [
+            { 'name': u'Leopard Debug (Build)', },
+            { 'name': u'Leopard Debug (Tests)', },
+            { 'name': u'Windows Release (Build)', },
+            { 'name': u'Windows Debug (Tests)', },
+            { 'name': u'Qt Linux Release', },
+        ]
+        name_regexps = [ 'Leopard', "Windows.*Build" ]
+        expected_builders = [
+            { 'name': u'Leopard Debug (Build)', },
+            { 'name': u'Leopard Debug (Tests)', },
+            { 'name': u'Windows Release (Build)', },
+        ]
+
+        # This test should probably be updated if the default regexp list changes
+        self.assertEquals(buildbot.core_builder_names_regexps, name_regexps)
+
+        builders = buildbot._builder_statuses_with_names_matching_regexps(example_builders, name_regexps)
+        self.assertEquals(builders, expected_builders)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/buildsteps.py b/WebKitTools/Scripts/webkitpy/buildsteps.py
new file mode 100644
index 0000000..230541e
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildsteps.py
@@ -0,0 +1,541 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import StringIO
+
+from optparse import make_option
+
+from webkitpy.comments import bug_comment_from_commit_text
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import log, error
+from webkitpy.webkitport import WebKitPort
+from webkitpy.changelogs import ChangeLog
+
+# FIXME: Why do some of these have "Step" in their name but not all?
+__all__ = [
+    "ApplyPatchStep",
+    "ApplyPatchWithLocalCommitStep",
+    "BuildStep",
+    "CheckStyleStep",
+    "CleanWorkingDirectoryStep",
+    "CleanWorkingDirectoryWithLocalCommitsStep",
+    "CloseBugForLandDiffStep",
+    "CloseBugStep",
+    "ClosePatchStep",
+    "CommitStep",
+    "CompleteRollout",
+    "CreateBugStep",
+    "EnsureBuildersAreGreenStep",
+    "EnsureLocalCommitIfNeeded",
+    "ObsoletePatchesOnBugStep",
+    "PostDiffToBugStep",
+    "PrepareChangeLogForRevertStep",
+    "PrepareChangeLogStep",
+    "PromptForBugOrTitleStep",
+    "RevertRevisionStep",
+    "RunTestsStep",
+    "UpdateChangeLogsWithReviewerStep",
+    "UpdateStep",
+]
+
+class CommandOptions(object):
+    force_clean = make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)")
+    clean = make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches")
+    check_builders = make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="Don't check to see if the build.webkit.org builders are green before landing.")
+    quiet = make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output.")
+    non_interactive = make_option("--non-interactive", action="store_true", dest="non_interactive", default=False, help="Never prompt the user, fail as fast as possible.")
+    parent_command = make_option("--parent-command", action="store", dest="parent_command", default=None, help="(Internal) The command that spawned this instance.")
+    update = make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory.")
+    local_commit = make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch")
+    build = make_option("--no-build", action="store_false", dest="build", default=True, help="Commit without building first, implies --no-test.")
+    build_style = make_option("--build-style", action="store", dest="build_style", default=None, help="Whether to build debug, release, or both.")
+    test = make_option("--no-test", action="store_false", dest="test", default=True, help="Commit without running run-webkit-tests.")
+    close_bug = make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing.")
+    port = make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...).")
+    reviewer = make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER.")
+    complete_rollout = make_option("--complete-rollout", action="store_true", dest="complete_rollout", help="Commit the revert and re-open the original bug.")
+    obsolete_patches = make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one.")
+    review = make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review.")
+    request_commit = make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review.")
+    description = make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: \"patch\")")
+    cc = make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy.")
+    component = make_option("--component", action="store", type="string", dest="component", help="Component for the new bug.")
+    confirm = make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Skip confirmation steps.")
+
+
+class AbstractStep(object):
+    def __init__(self, tool, options):
+        self._tool = tool
+        self._options = options
+        self._port = None
+
+    def _run_script(self, script_name, quiet=False, port=WebKitPort):
+        log("Running %s" % script_name)
+        # FIXME: This should use self.port()
+        self._tool.executive.run_and_throw_if_fail(port.script_path(script_name), quiet)
+
+    # FIXME: The port should live on the tool.
+    def port(self):
+        if self._port:
+            return self._port
+        self._port = WebKitPort.port(self._options.port)
+        return self._port
+
+    @classmethod
+    def options(cls):
+        return []
+
+    def run(self, state):
+        raise NotImplementedError, "subclasses must implement"
+
+
+# FIXME: Unify with StepSequence?  I'm not sure yet which is the better design.
+class MetaStep(AbstractStep):
+    substeps = [] # Override in subclasses
+    def __init__(self, tool, options):
+        AbstractStep.__init__(self, tool, options)
+        self._step_instances = []
+        for step_class in self.substeps:
+            self._step_instances.append(step_class(tool, options))
+
+    @staticmethod
+    def _collect_options_from_steps(steps):
+        collected_options = []
+        for step in steps:
+            collected_options = collected_options + step.options()
+        return collected_options
+
+    @classmethod
+    def options(cls):
+        return cls._collect_options_from_steps(cls.substeps)
+
+    def run(self, state):
+        for step in self._step_instances:
+             step.run(state)
+
+
+class PromptForBugOrTitleStep(AbstractStep):
+    def run(self, state):
+        # No need to prompt if we alrady have the bug_id.
+        if state.get("bug_id"):
+            return
+        user_response = self._tool.user.prompt("Please enter a bug number or a title for a new bug:\n")
+        # If the user responds with a number, we assume it's bug number.
+        # Otherwise we assume it's a bug subject.
+        try:
+            state["bug_id"] = int(user_response)
+        except ValueError, TypeError:
+            state["bug_title"] = user_response
+            # FIXME: This is kind of a lame description.
+            state["bug_description"] = user_response
+
+
+class CreateBugStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.cc,
+            CommandOptions.component,
+        ]
+
+    def run(self, state):
+        # No need to create a bug if we already have one.
+        if state.get("bug_id"):
+            return
+        state["bug_id"] = self._tool.bugs.create_bug(state["bug_title"], state["bug_description"], component=self._options.component, cc=self._options.cc)
+
+
+class PrepareChangeLogStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.port,
+            CommandOptions.quiet,
+        ]
+
+    def run(self, state):
+        os.chdir(self._tool.scm().checkout_root)
+        args = [self.port().script_path("prepare-ChangeLog")]
+        if state["bug_id"]:
+            args.append("--bug=%s" % state["bug_id"])
+        self._tool.executive.run_and_throw_if_fail(args, self._options.quiet)
+
+
+class EditChangeLogStep(AbstractStep):
+    def run(self, state):
+        self._tool.user.edit(self._tool.scm().modified_changelogs())
+
+
+class ObsoletePatchesOnBugStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.obsolete_patches,
+        ]
+
+    def run(self, state):
+        if not self._options.obsolete_patches:
+            return
+        bug_id = state["bug_id"]
+        patches = self._tool.bugs.fetch_patches_from_bug(bug_id)
+        if not patches:
+            return
+        log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
+        for patch in patches:
+            self._tool.bugs.obsolete_attachment(patch["id"])
+
+
+class AbstractDiffStep(AbstractStep):
+    def diff(self, state):
+        diff = state.get("diff")
+        if not diff:
+            diff = self._tool.scm().create_patch()
+            state["diff"] = diff
+        return diff
+
+
+class ConfirmDiffStep(AbstractDiffStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.confirm,
+        ]
+
+    def run(self, state):
+        if not self._options.confirm:
+            return
+        diff = self.diff(state)
+        self._tool.user.page(diff)
+        if not self._tool.user.confirm():
+            error("User declined to continue.")
+
+
+class PostDiffToBugStep(AbstractDiffStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.description,
+            CommandOptions.review,
+            CommandOptions.request_commit,
+        ]
+
+    def run(self, state):
+        diff = self.diff(state)
+        diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
+        description = self._options.description or "Patch"
+        self._tool.bugs.add_patch_to_bug(state["bug_id"], diff_file, description, mark_for_review=self._options.review, mark_for_commit_queue=self._options.request_commit)
+
+
+class PrepareChangeLogForRevertStep(AbstractStep):
+    def run(self, state):
+        # First, discard the ChangeLog changes from the rollout.
+        os.chdir(self._tool.scm().checkout_root)
+        changelog_paths = self._tool.scm().modified_changelogs()
+        self._tool.scm().revert_files(changelog_paths)
+
+        # Second, make new ChangeLog entries for this rollout.
+        # This could move to prepare-ChangeLog by adding a --revert= option.
+        self._run_script("prepare-ChangeLog")
+        for changelog_path in changelog_paths:
+            ChangeLog(changelog_path).update_for_revert(state["revision"])
+
+
+class CleanWorkingDirectoryStep(AbstractStep):
+    def __init__(self, tool, options, allow_local_commits=False):
+        AbstractStep.__init__(self, tool, options)
+        self._allow_local_commits = allow_local_commits
+
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.force_clean,
+            CommandOptions.clean,
+        ]
+
+    def run(self, state):
+        os.chdir(self._tool.scm().checkout_root)
+        if not self._allow_local_commits:
+            self._tool.scm().ensure_no_local_commits(self._options.force_clean)
+        if self._options.clean:
+            self._tool.scm().ensure_clean_working_directory(force_clean=self._options.force_clean)
+
+
+class CleanWorkingDirectoryWithLocalCommitsStep(CleanWorkingDirectoryStep):
+    def __init__(self, tool, options):
+        # FIXME: This a bit of a hack.  Consider doing this more cleanly.
+        CleanWorkingDirectoryStep.__init__(self, tool, options, allow_local_commits=True)
+
+
+class UpdateStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.update,
+            CommandOptions.port,
+        ]
+
+    def run(self, state):
+        if not self._options.update:
+            return
+        log("Updating working directory")
+        self._tool.executive.run_and_throw_if_fail(self.port().update_webkit_command(), quiet=True)
+
+
+class ApplyPatchStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.non_interactive,
+        ]
+
+    def run(self, state):
+        log("Processing patch %s from bug %s." % (state["patch"]["id"], state["patch"]["bug_id"]))
+        self._tool.scm().apply_patch(state["patch"], force=self._options.non_interactive)
+
+
+class RevertRevisionStep(AbstractStep):
+    def run(self, state):
+        self._tool.scm().apply_reverse_diff(state["revision"])
+
+
+class ApplyPatchWithLocalCommitStep(ApplyPatchStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.local_commit,
+        ] + ApplyPatchStep.options()
+    
+    def run(self, state):
+        ApplyPatchStep.run(self, state)
+        if self._options.local_commit:
+            commit_message = self._tool.scm().commit_message_for_this_commit()
+            self._tool.scm().commit_locally_with_message(commit_message.message() or state["patch"]["name"])
+
+
+class EnsureBuildersAreGreenStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.check_builders,
+        ]
+
+    def run(self, state):
+        if not self._options.check_builders:
+            return
+        red_builders_names = self._tool.buildbot.red_core_builders_names()
+        if not red_builders_names:
+            return
+        red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
+        error("Builders [%s] are red, please do not commit.\nSee http://%s.\nPass --ignore-builders to bypass this check." % (", ".join(red_builders_names), self._tool.buildbot.buildbot_host))
+
+
+class EnsureLocalCommitIfNeeded(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.local_commit,
+        ]
+
+    def run(self, state):
+        if self._options.local_commit and not self._tool.scm().supports_local_commits():
+            error("--local-commit passed, but %s does not support local commits" % self._tool.scm.display_name())
+
+
+class UpdateChangeLogsWithReviewerStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.reviewer,
+        ]
+
+    def _guess_reviewer_from_bug(self, bug_id):
+        patches = self._tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+        if len(patches) != 1:
+            log("%s on bug %s, cannot infer reviewer." % (pluralize("reviewed patch", len(patches)), bug_id))
+            return None
+        patch = patches[0]
+        reviewer = patch["reviewer"]
+        log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (reviewer, patch["id"], bug_id))
+        return reviewer
+
+    def run(self, state):
+        bug_id = state["patch"]["bug_id"]
+        reviewer = self._options.reviewer
+        if not reviewer:
+            if not bug_id:
+                log("No bug id provided and --reviewer= not provided.  Not updating ChangeLogs with reviewer.")
+                return
+            reviewer = self._guess_reviewer_from_bug(bug_id)
+
+        if not reviewer:
+            log("Failed to guess reviewer from bug %s and --reviewer= not provided.  Not updating ChangeLogs with reviewer." % bug_id)
+            return
+
+        os.chdir(self._tool.scm().checkout_root)
+        for changelog_path in self._tool.scm().modified_changelogs():
+            ChangeLog(changelog_path).set_reviewer(reviewer)
+
+
+class BuildStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.build,
+            CommandOptions.quiet,
+            CommandOptions.build_style,
+        ]
+
+    def build(self, build_style):
+        self._tool.executive.run_and_throw_if_fail(self.port().build_webkit_command(build_style=build_style), self._options.quiet)
+
+    def run(self, state):
+        if not self._options.build:
+            return
+        log("Building WebKit")
+        if self._options.build_style == "both":
+            self.build("debug")
+            self.build("release")
+        else:
+            self.build(self._options.build_style)
+
+
+class CheckStyleStep(AbstractStep):
+    def run(self, state):
+        self._run_script("check-webkit-style")
+
+
+class RunTestsStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.build,
+            CommandOptions.test,
+            CommandOptions.non_interactive,
+            CommandOptions.quiet,
+            CommandOptions.port,
+        ]
+
+    def run(self, state):
+        if not self._options.build:
+            return
+        if not self._options.test:
+            return
+        args = self.port().run_webkit_tests_command()
+        if self._options.non_interactive:
+            args.append("--no-launch-safari")
+            args.append("--exit-after-n-failures=1")
+        if self._options.quiet:
+            args.append("--quiet")
+        self._tool.executive.run_and_throw_if_fail(args)
+
+
+class CommitStep(AbstractStep):
+    def run(self, state):
+        commit_message = self._tool.scm().commit_message_for_this_commit()
+        state["commit_text"] =  self._tool.scm().commit_with_message(commit_message.message())
+
+
+class ClosePatchStep(AbstractStep):
+    def run(self, state):
+        comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+        self._tool.bugs.clear_attachment_flags(state["patch"]["id"], comment_text)
+
+
+class CloseBugStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.close_bug,
+        ]
+
+    def run(self, state):
+        if not self._options.close_bug:
+            return
+        # Check to make sure there are no r? or r+ patches on the bug before closing.
+        # Assume that r- patches are just previous patches someone forgot to obsolete.
+        patches = self._tool.bugs.fetch_patches_from_bug(state["patch"]["bug_id"])
+        for patch in patches:
+            review_flag = patch.get("review")
+            if review_flag == "?" or review_flag == "+":
+                log("Not closing bug %s as attachment %s has review=%s.  Assuming there are more patches to land from this bug." % (patch["bug_id"], patch["id"], review_flag))
+                return
+        self._tool.bugs.close_bug_as_fixed(state["patch"]["bug_id"], "All reviewed patches have been landed.  Closing bug.")
+
+
+class CloseBugForLandDiffStep(AbstractStep):
+    @classmethod
+    def options(cls):
+        return [
+            CommandOptions.close_bug,
+        ]
+
+    def run(self, state):
+        comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+        bug_id = state["patch"]["bug_id"]
+        if bug_id:
+            log("Updating bug %s" % bug_id)
+            if self._options.close_bug:
+                self._tool.bugs.close_bug_as_fixed(bug_id, comment_text)
+            else:
+                # FIXME: We should a smart way to figure out if the patch is attached
+                # to the bug, and if so obsolete it.
+                self._tool.bugs.post_comment_to_bug(bug_id, comment_text)
+        else:
+            log(comment_text)
+            log("No bug id provided.")
+
+
+class CompleteRollout(MetaStep):
+    substeps = [
+        BuildStep,
+        CommitStep,
+    ]
+
+    @classmethod
+    def options(cls):
+        collected_options = cls._collect_options_from_steps(cls.substeps)
+        collected_options.append(CommandOptions.complete_rollout)
+        return collected_options
+
+    def run(self, state):
+        bug_id = state["bug_id"]
+        # FIXME: Fully automated rollout is not 100% idiot-proof yet, so for now just log with instructions on how to complete the rollout.
+        # Once we trust rollout we will remove this option.
+        if not self._options.complete_rollout:
+            log("\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff %s\" to commit the rollout." % bug_id)
+            return
+
+        MetaStep.run(self, state)
+
+        if not bug_id:
+            log(state["commit_text"])
+            log("No bugs were updated or re-opened to reflect this rollout.")
+            return
+        # FIXME: I'm not sure state["commit_text"] is quite right here.
+        self._tool.bugs.reopen_bug(bug_id, state["commit_text"])
diff --git a/WebKitTools/Scripts/webkitpy/buildsteps_unittest.py b/WebKitTools/Scripts/webkitpy/buildsteps_unittest.py
new file mode 100644
index 0000000..158f4c0
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildsteps_unittest.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.buildsteps import UpdateChangeLogsWithReviewerStep, UpdateStep, PromptForBugOrTitleStep
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+from webkitpy.mock import Mock
+
+
+class UpdateChangeLogsWithReviewerStepTest(unittest.TestCase):
+    def test_guess_reviewer_from_bug(self):
+        capture = OutputCapture()
+        step = UpdateChangeLogsWithReviewerStep(MockBugzillaTool(), [])
+        expected_stderr = "0 reviewed patches on bug 75, cannot infer reviewer.\n"
+        capture.assert_outputs(self, step._guess_reviewer_from_bug, [75], expected_stderr=expected_stderr)
+
+
+class StepsTest(unittest.TestCase):
+    def _run_step(self, step, tool=None, options=None, state=None):
+        if not tool:
+            tool = MockBugzillaTool()
+        if not options:
+            options = Mock()
+        if not state:
+            state = {}
+        step(tool, options).run(state)
+
+    def test_update_step(self):
+        options = Mock()
+        options.update = True
+        self._run_step(UpdateStep, options)
+
+    def test_prompt_for_bug_or_title_step(self):
+        tool = MockBugzillaTool()
+        tool.user.prompt = lambda message: 42
+        self._run_step(PromptForBugOrTitleStep, tool=tool)
diff --git a/WebKitTools/Scripts/modules/changelogs.py b/WebKitTools/Scripts/webkitpy/changelogs.py
similarity index 100%
rename from WebKitTools/Scripts/modules/changelogs.py
rename to WebKitTools/Scripts/webkitpy/changelogs.py
diff --git a/WebKitTools/Scripts/modules/changelogs_unittest.py b/WebKitTools/Scripts/webkitpy/changelogs_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/changelogs_unittest.py
rename to WebKitTools/Scripts/webkitpy/changelogs_unittest.py
diff --git a/WebKitTools/Scripts/modules/commands/__init__.py b/WebKitTools/Scripts/webkitpy/commands/__init__.py
similarity index 100%
rename from WebKitTools/Scripts/modules/commands/__init__.py
rename to WebKitTools/Scripts/webkitpy/commands/__init__.py
diff --git a/WebKitTools/Scripts/webkitpy/commands/commandtest.py b/WebKitTools/Scripts/webkitpy/commands/commandtest.py
new file mode 100644
index 0000000..a56cb05
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/commandtest.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.mock import Mock
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+
+class CommandsTest(unittest.TestCase):
+    def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=Mock(), tool=MockBugzillaTool()):
+        command.bind_to_tool(tool)
+        OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/download.py b/WebKitTools/Scripts/webkitpy/commands/download.py
new file mode 100644
index 0000000..8d4b2bf
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/download.py
@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from optparse import make_option
+
+from webkitpy.bugzilla import parse_bug_id
+# We could instead use from modules import buildsteps and then prefix every buildstep with "buildsteps."
+from webkitpy.buildsteps import *
+from webkitpy.changelogs import ChangeLog
+from webkitpy.comments import bug_comment_from_commit_text
+from webkitpy.executive import ScriptError
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import error, log
+from webkitpy.multicommandtool import AbstractDeclarativeCommmand, Command
+from webkitpy.stepsequence import StepSequence
+
+
+# FIXME: Move this to a more general location.
+class AbstractSequencedCommmand(AbstractDeclarativeCommmand):
+    steps = None
+    def __init__(self):
+        self._sequence = StepSequence(self.steps)
+        AbstractDeclarativeCommmand.__init__(self, self._sequence.options())
+
+    def _prepare_state(self, options, args, tool):
+        return None
+
+    def execute(self, options, args, tool):
+        self._sequence.run_and_handle_errors(tool, options, self._prepare_state(options, args, tool))
+
+
+class Build(AbstractSequencedCommmand):
+    name = "build"
+    help_text = "Update working copy and build"
+    steps = [
+        CleanWorkingDirectoryStep,
+        UpdateStep,
+        BuildStep,
+    ]
+
+
+class BuildAndTest(AbstractSequencedCommmand):
+    name = "build-and-test"
+    help_text = "Update working copy, build, and run the tests"
+    steps = [
+        CleanWorkingDirectoryStep,
+        UpdateStep,
+        BuildStep,
+        RunTestsStep,
+    ]
+
+
+class LandDiff(AbstractSequencedCommmand):
+    name = "land-diff"
+    help_text = "Land the current working directory diff and updates the associated bug if any"
+    argument_names = "[BUGID]"
+    show_in_main_help = True
+    steps = [
+        EnsureBuildersAreGreenStep,
+        UpdateChangeLogsWithReviewerStep,
+        EnsureBuildersAreGreenStep,
+        BuildStep,
+        RunTestsStep,
+        CommitStep,
+        CloseBugForLandDiffStep,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        return {
+            "patch" : {
+                "id" : None,
+                "bug_id" : (args and args[0]) or parse_bug_id(tool.scm().create_patch()),
+            }
+        }
+
+
+class AbstractPatchProcessingCommand(AbstractDeclarativeCommmand):
+    # Subclasses must implement the methods below.  We don't declare them here
+    # because we want to be able to implement them with mix-ins.
+    #
+    # def _fetch_list_of_patches_to_process(self, options, args, tool):
+    # def _prepare_to_process(self, options, args, tool):
+
+    @staticmethod
+    def _collect_patches_by_bug(patches):
+        bugs_to_patches = {}
+        for patch in patches:
+            bug_id = patch["bug_id"]
+            bugs_to_patches[bug_id] = bugs_to_patches.get(bug_id, []) + [patch]
+        return bugs_to_patches
+
+    def execute(self, options, args, tool):
+        self._prepare_to_process(options, args, tool)
+        patches = self._fetch_list_of_patches_to_process(options, args, tool)
+
+        # It's nice to print out total statistics.
+        bugs_to_patches = self._collect_patches_by_bug(patches)
+        log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
+
+        for patch in patches:
+            self._process_patch(patch, options, args, tool)
+
+
+class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
+    prepare_steps = None
+    main_steps = None
+
+    def __init__(self):
+        options = []
+        self._prepare_sequence = StepSequence(self.prepare_steps)
+        self._main_sequence = StepSequence(self.main_steps)
+        options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
+        AbstractPatchProcessingCommand.__init__(self, options)
+
+    def _prepare_to_process(self, options, args, tool):
+        self._prepare_sequence.run_and_handle_errors(tool, options)
+
+    def _process_patch(self, patch, options, args, tool):
+        state = { "patch" : patch }
+        self._main_sequence.run_and_handle_errors(tool, options, state)
+
+
+class ProcessAttachmentsMixin(object):
+    def _fetch_list_of_patches_to_process(self, options, args, tool):
+        return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
+
+
+class ProcessBugsMixin(object):
+    def _fetch_list_of_patches_to_process(self, options, args, tool):
+        all_patches = []
+        for bug_id in args:
+            patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+            log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
+            all_patches += patches
+        return all_patches
+
+
+class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+    name = "check-style"
+    help_text = "Run check-webkit-style on the specified attachments"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    main_steps = [
+        CleanWorkingDirectoryStep,
+        UpdateStep,
+        ApplyPatchStep,
+        CheckStyleStep,
+    ]
+
+
+class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+    name = "build-attachment"
+    help_text = "Apply and build patches from bugzilla"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    main_steps = [
+        CleanWorkingDirectoryStep,
+        UpdateStep,
+        ApplyPatchStep,
+        BuildStep,
+    ]
+
+
+class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
+    prepare_steps = [
+        EnsureLocalCommitIfNeeded,
+        CleanWorkingDirectoryWithLocalCommitsStep,
+        UpdateStep,
+    ]
+    main_steps = [
+        ApplyPatchWithLocalCommitStep,
+    ]
+
+
+class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
+    name = "apply-attachment"
+    help_text = "Apply an attachment to the local working directory"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    show_in_main_help = True
+
+
+class ApplyPatches(AbstractPatchApplyingCommand, ProcessBugsMixin):
+    name = "apply-patches"
+    help_text = "Apply reviewed patches from provided bugs to the local working directory"
+    argument_names = "BUGID [BUGIDS]"
+    show_in_main_help = True
+
+
+class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
+    prepare_steps = [
+        EnsureBuildersAreGreenStep,
+    ]
+    main_steps = [
+        CleanWorkingDirectoryStep,
+        UpdateStep,
+        ApplyPatchStep,
+        EnsureBuildersAreGreenStep,
+        BuildStep,
+        RunTestsStep,
+        CommitStep,
+        ClosePatchStep,
+        CloseBugStep,
+    ]
+
+
+class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
+    name = "land-attachment"
+    help_text = "Land patches from bugzilla, optionally building and testing them first"
+    argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+    show_in_main_help = True
+
+
+class LandPatches(AbstractPatchLandingCommand, ProcessBugsMixin):
+    name = "land-patches"
+    help_text = "Land all patches on the given bugs, optionally building and testing them first"
+    argument_names = "BUGID [BUGIDS]"
+    show_in_main_help = True
+
+
+# FIXME: Make Rollout more declarative.
+class Rollout(Command):
+    name = "rollout"
+    show_in_main_help = True
+    def __init__(self):
+        self._sequence = StepSequence([
+            CleanWorkingDirectoryStep,
+            UpdateStep,
+            RevertRevisionStep,
+            PrepareChangeLogForRevertStep,
+            CompleteRollout,
+        ])
+        Command.__init__(self, "Revert the given revision in the working copy and optionally commit the revert and re-open the original bug", "REVISION [BUGID]", options=self._sequence.options())
+
+    @staticmethod
+    def _parse_bug_id_from_revision_diff(tool, revision):
+        original_diff = tool.scm().diff_for_revision(revision)
+        return parse_bug_id(original_diff)
+
+    @staticmethod
+    def _reopen_bug_after_rollout(tool, bug_id, comment_text):
+        if bug_id:
+            tool.bugs.reopen_bug(bug_id, comment_text)
+        else:
+            log(comment_text)
+            log("No bugs were updated or re-opened to reflect this rollout.")
+
+    def execute(self, options, args, tool):
+        revision = args[0]
+        bug_id = self._parse_bug_id_from_revision_diff(tool, revision)
+        if options.complete_rollout:
+            if bug_id:
+                log("Will re-open bug %s after rollout." % bug_id)
+            else:
+                log("Failed to parse bug number from diff.  No bugs will be updated/reopened after the rollout.")
+
+        state = {
+            "revision": revision,
+            "bug_id": bug_id,
+        }
+        self._sequence.run_and_handle_errors(tool, options, state)
diff --git a/WebKitTools/Scripts/webkitpy/commands/download_unittest.py b/WebKitTools/Scripts/webkitpy/commands/download_unittest.py
new file mode 100644
index 0000000..d914425
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/download_unittest.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.commands.commandtest import CommandsTest
+from webkitpy.commands.download import *
+from webkitpy.mock import Mock
+
+class DownloadCommandsTest(CommandsTest):
+    def _default_options(self):
+        options = Mock()
+        options.force_clean = False
+        options.clean = True
+        options.check_builders = True
+        options.quiet = False
+        options.non_interactive = False
+        options.update = True
+        options.build = True
+        options.test = True
+        options.close_bug = True
+        options.complete_rollout = False
+        return options
+
+    def test_build(self):
+        expected_stderr = "Updating working directory\nBuilding WebKit\n"
+        self.assert_execute_outputs(Build(), [], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_build_and_test(self):
+        expected_stderr = "Updating working directory\nBuilding WebKit\n"
+        self.assert_execute_outputs(BuildAndTest(), [], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_apply_attachment(self):
+        options = self._default_options()
+        options.update = True
+        options.local_commit = True
+        expected_stderr = "Updating working directory\nProcessing 1 patch from 1 bug.\nProcessing patch 197 from bug 42.\n"
+        self.assert_execute_outputs(ApplyAttachment(), [197], options=options, expected_stderr=expected_stderr)
+
+    def test_apply_patches(self):
+        options = self._default_options()
+        options.update = True
+        options.local_commit = True
+        expected_stderr = "Updating working directory\n2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nProcessing patch 197 from bug 42.\nProcessing patch 128 from bug 42.\n"
+        self.assert_execute_outputs(ApplyPatches(), [42], options=options, expected_stderr=expected_stderr)
+
+    def test_land_diff(self):
+        expected_stderr = "Building WebKit\nUpdating bug 42\n"
+        self.assert_execute_outputs(LandDiff(), [42], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_check_style(self):
+        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nRunning check-webkit-style\n"
+        self.assert_execute_outputs(CheckStyle(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_build_attachment(self):
+        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
+        self.assert_execute_outputs(BuildAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_land_attachment(self):
+        expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
+        self.assert_execute_outputs(LandAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_land_patches(self):
+        expected_stderr = "2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\nUpdating working directory\nProcessing patch 128 from bug 42.\nBuilding WebKit\n"
+        self.assert_execute_outputs(LandPatches(), [42], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_rollout(self):
+        expected_stderr = "Updating working directory\nRunning prepare-ChangeLog\n\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff 12345\" to commit the rollout.\n"
+        self.assert_execute_outputs(Rollout(), [852], options=self._default_options(), expected_stderr=expected_stderr)
+
+    def test_complete_rollout(self):
+        options = self._default_options()
+        options.complete_rollout = True
+        expected_stderr = "Will re-open bug 12345 after rollout.\nUpdating working directory\nRunning prepare-ChangeLog\nBuilding WebKit\n"
+        self.assert_execute_outputs(Rollout(), [852], options=options, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/early_warning_system.py b/WebKitTools/Scripts/webkitpy/commands/early_warning_system.py
new file mode 100644
index 0000000..0d69c49
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/early_warning_system.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from StringIO import StringIO
+
+from webkitpy.commands.queues import AbstractReviewQueue
+from webkitpy.committers import CommitterList
+from webkitpy.executive import ScriptError
+from webkitpy.webkitport import WebKitPort
+
+
+class AbstractEarlyWarningSystem(AbstractReviewQueue):
+    def __init__(self):
+        AbstractReviewQueue.__init__(self)
+        self.port = WebKitPort.port(self.port_name)
+
+    def should_proceed_with_work_item(self, patch):
+        try:
+            self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
+            self._update_status("Building", patch)
+        except ScriptError, e:
+            self._update_status("Unable to perform a build")
+            return False
+        return True
+
+    def process_work_item(self, patch):
+        try:
+            self.run_bugzilla_tool([
+                "build-attachment",
+                self.port.flag(),
+                "--force-clean",
+                "--quiet",
+                "--non-interactive",
+                "--parent-command=%s" % self.name,
+                "--no-update",
+                patch["id"]])
+            self._did_pass(patch)
+        except ScriptError, e:
+            self._did_fail(patch)
+            raise e
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        status_id = cls._update_status_for_script_error(tool, state, script_error)
+        # FIXME: This won't be right for ports that don't use build-webkit!
+        if not script_error.command_name() == "build-webkit":
+            return
+        results_link = tool.status_bot.results_url_for_status(status_id)
+        message = "Attachment %s did not build on %s:\nBuild output: %s" % (state["patch"]["id"], cls.port_name, results_link)
+        tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
+
+
+class GtkEWS(AbstractEarlyWarningSystem):
+    name = "gtk-ews"
+    port_name = "gtk"
+
+
+class QtEWS(AbstractEarlyWarningSystem):
+    name = "qt-ews"
+    port_name = "qt"
+
+
+class ChromiumEWS(AbstractEarlyWarningSystem):
+    name = "chromium-ews"
+    port_name = "chromium"
+    watchers = AbstractEarlyWarningSystem.watchers + [
+        "dglazkov at chromium.org",
+    ]
+
+
+# For platforms that we can't run inside a VM (like Mac OS X), we require
+# patches to be uploaded by committers, who are generally trustworthy folk. :)
+class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem):
+    def __init__(self, committers=CommitterList()):
+        AbstractEarlyWarningSystem.__init__(self)
+        self._committers = committers
+
+    def process_work_item(self, patch):
+        if not self._committers.committer_by_email(patch["attacher_email"]):
+            self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name)
+            return
+        AbstractEarlyWarningSystem.process_work_item(self, patch)
+
+
+class MacEWS(AbstractCommitterOnlyEWS):
+    name = "mac-ews"
+    port_name = "mac"
diff --git a/WebKitTools/Scripts/webkitpy/commands/early_warning_system_unittest.py b/WebKitTools/Scripts/webkitpy/commands/early_warning_system_unittest.py
new file mode 100644
index 0000000..dec8981
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/early_warning_system_unittest.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import unittest
+
+from webkitpy.commands.early_warning_system import *
+from webkitpy.commands.queuestest import QueuesTest
+from webkitpy.mock import Mock
+
+class EarlyWarningSytemTest(QueuesTest):
+    def test_chromium_ews(self):
+        expected_stderr = {
+            "begin_work_queue" : "CAUTION: chromium-ews will discard all local changes in \"%s\"\nRunning WebKit chromium-ews.\n" % os.getcwd(),
+            "handle_unexpected_error" : "Mock error message\n",
+        }
+        self.assert_queue_outputs(ChromiumEWS(), expected_stderr=expected_stderr)
+
+    def test_qt_ews(self):
+        expected_stderr = {
+            "begin_work_queue" : "CAUTION: qt-ews will discard all local changes in \"%s\"\nRunning WebKit qt-ews.\n" % os.getcwd(),
+            "handle_unexpected_error" : "Mock error message\n",
+        }
+        self.assert_queue_outputs(QtEWS(), expected_stderr=expected_stderr)
+
+    def test_gtk_ews(self):
+        expected_stderr = {
+            "begin_work_queue" : "CAUTION: gtk-ews will discard all local changes in \"%s\"\nRunning WebKit gtk-ews.\n" % os.getcwd(),
+            "handle_unexpected_error" : "Mock error message\n",
+        }
+        self.assert_queue_outputs(GtkEWS(), expected_stderr=expected_stderr)
+
+    def test_mac_ews(self):
+        expected_stderr = {
+            "begin_work_queue" : "CAUTION: mac-ews will discard all local changes in \"%s\"\nRunning WebKit mac-ews.\n" % os.getcwd(),
+            "handle_unexpected_error" : "Mock error message\n",
+        }
+        self.assert_queue_outputs(MacEWS(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queries.py b/WebKitTools/Scripts/webkitpy/commands/queries.py
new file mode 100644
index 0000000..c9577ba
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queries.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from optparse import make_option
+
+from webkitpy.buildbot import BuildBot
+from webkitpy.committers import CommitterList
+from webkitpy.webkit_logging import log
+from webkitpy.multicommandtool import Command
+
+
+class BugsToCommit(Command):
+    name = "bugs-to-commit"
+    def __init__(self):
+        Command.__init__(self, "List bugs in the commit-queue")
+
+    def execute(self, options, args, tool):
+        bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
+        for bug_id in bug_ids:
+            print "%s" % bug_id
+
+
+class PatchesToCommit(Command):
+    name = "patches-to-commit"
+    def __init__(self):
+        Command.__init__(self, "List patches in the commit-queue")
+
+    def execute(self, options, args, tool):
+        patches = tool.bugs.queries.fetch_patches_from_commit_queue()
+        log("Patches in commit queue:")
+        for patch in patches:
+            print "%s" % patch["url"]
+
+
+class PatchesToCommitQueue(Command):
+    name = "patches-to-commit-queue"
+    def __init__(self):
+        options = [
+            make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
+        ]
+        Command.__init__(self, "Patches which should be added to the commit queue", options=options)
+
+    @staticmethod
+    def _needs_commit_queue(patch):
+        commit_queue_flag = patch.get("commit-queue")
+        if (commit_queue_flag and commit_queue_flag == '+'): # If it's already cq+, ignore the patch.
+            log("%s already has cq=%s" % (patch["id"], commit_queue_flag))
+            return False
+
+        # We only need to worry about patches from contributers who are not yet committers.
+        committer_record = CommitterList().committer_by_email(patch["attacher_email"])
+        if committer_record:
+            log("%s committer = %s" % (patch["id"], committer_record))
+        return not committer_record
+
+    def execute(self, options, args, tool):
+        patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
+        patches_needing_cq = filter(self._needs_commit_queue, patches)
+        if options.bugs:
+            bugs_needing_cq = map(lambda patch: patch['bug_id'], patches_needing_cq)
+            bugs_needing_cq = sorted(set(bugs_needing_cq))
+            for bug_id in bugs_needing_cq:
+                print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
+        else:
+            for patch in patches_needing_cq:
+                print "%s" % tool.bugs.attachment_url_for_id(patch["id"], action="edit")
+
+
+class PatchesToReview(Command):
+    name = "patches-to-review"
+    def __init__(self):
+        Command.__init__(self, "List patches that are pending review")
+
+    def execute(self, options, args, tool):
+        patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue()
+        log("Patches pending review:")
+        for patch_id in patch_ids:
+            print patch_id
+
+
+class ReviewedPatches(Command):
+    name = "reviewed-patches"
+    def __init__(self):
+        Command.__init__(self, "List r+'d patches on a bug", "BUGID")
+
+    def execute(self, options, args, tool):
+        bug_id = args[0]
+        patches_to_land = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+        for patch in patches_to_land:
+            print "%s" % patch["url"]
+
+
+class TreeStatus(Command):
+    name = "tree-status"
+    show_in_main_help = True
+    def __init__(self):
+        Command.__init__(self, "Print the status of the %s buildbots" % BuildBot.default_host)
+
+    def execute(self, options, args, tool):
+        for builder in tool.buildbot.builder_statuses():
+            status_string = "ok" if builder["is_green"] else "FAIL"
+            print "%s : %s" % (status_string.ljust(4), builder["name"])
diff --git a/WebKitTools/Scripts/webkitpy/commands/queries_unittest.py b/WebKitTools/Scripts/webkitpy/commands/queries_unittest.py
new file mode 100644
index 0000000..c7fd7a8
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queries_unittest.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.bugzilla import Bugzilla
+from webkitpy.commands.commandtest import CommandsTest
+from webkitpy.commands.queries import *
+from webkitpy.mock import Mock
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+
+class QueryCommandsTest(CommandsTest):
+    def test_bugs_to_commit(self):
+        self.assert_execute_outputs(BugsToCommit(), None, "42\n75\n")
+
+    def test_patches_to_commit(self):
+        expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
+        expected_stderr = "Patches in commit queue:\n"
+        self.assert_execute_outputs(PatchesToCommit(), None, expected_stdout, expected_stderr)
+
+    def test_patches_to_commit_queue(self):
+        expected_stdout = "http://example.com/197&action=edit\n"
+        expected_stderr = "128 committer = \"Eric Seidel\" <eric at webkit.org>\n"
+        options = Mock()
+        options.bugs = False
+        self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+        expected_stdout = "http://example.com/42\n"
+        options.bugs = True
+        self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+    def test_patches_to_review(self):
+        expected_stdout = "197\n128\n"
+        expected_stderr = "Patches pending review:\n"
+        self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr)
+
+    def test_reviewed_patches(self):
+        expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
+        self.assert_execute_outputs(ReviewedPatches(), [42], expected_stdout)
+
+    def test_tree_status(self):
+        expected_stdout = "ok   : Builder1\nok   : Builder2\n"
+        self.assert_execute_outputs(TreeStatus(), None, expected_stdout)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queues.py b/WebKitTools/Scripts/webkitpy/commands/queues.py
new file mode 100644
index 0000000..e6f4f1b
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queues.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from datetime import datetime
+from optparse import make_option
+from StringIO import StringIO
+
+from webkitpy.executive import ScriptError
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import error, log
+from webkitpy.multicommandtool import Command
+from webkitpy.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate
+from webkitpy.statusbot import StatusBot
+from webkitpy.stepsequence import StepSequenceErrorHandler
+from webkitpy.queueengine import QueueEngine, QueueEngineDelegate
+
+class AbstractQueue(Command, QueueEngineDelegate):
+    watchers = [
+        "webkit-bot-watchers at googlegroups.com",
+    ]
+
+    _pass_status = "Pass"
+    _fail_status = "Fail"
+    _error_status = "Error"
+
+    def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
+        options_list = (options or []) + [
+            make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue.  Dangerous!"),
+        ]
+        Command.__init__(self, "Run the %s" % self.name, options=options_list)
+
+    def _cc_watchers(self, bug_id):
+        try:
+            self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
+        except Exception, e:
+            log("Failed to CC watchers: %s." % e)
+
+    def _update_status(self, message, patch=None, results_file=None):
+        self.tool.status_bot.update_status(self.name, message, patch, results_file)
+
+    def _did_pass(self, patch):
+        self._update_status(self._pass_status, patch)
+
+    def _did_fail(self, patch):
+        self._update_status(self._fail_status, patch)
+
+    def _did_error(self, patch, reason):
+        message = "%s: %s" % (self._error_status, reason)
+        self._update_status(message, patch)
+
+    def queue_log_path(self):
+        return "%s.log" % self.name
+
+    def work_item_log_path(self, patch):
+        return os.path.join("%s-logs" % self.name, "%s.log" % patch["bug_id"])
+
+    def begin_work_queue(self):
+        log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root))
+        if self.options.confirm:
+            response = raw_input("Are you sure?  Type \"yes\" to continue: ")
+            if (response != "yes"):
+                error("User declined.")
+        log("Running WebKit %s." % self.name)
+
+    def should_continue_work_queue(self):
+        return True
+
+    def next_work_item(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def should_proceed_with_work_item(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def process_work_item(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def handle_unexpected_error(self, work_item, message):
+        raise NotImplementedError, "subclasses must implement"
+
+    def run_bugzilla_tool(self, args):
+        bugzilla_tool_args = [self.tool.path()]
+        # FIXME: This is a hack, we should have a more general way to pass global options.
+        bugzilla_tool_args += ["--status-host=%s" % self.tool.status_bot.statusbot_host]
+        bugzilla_tool_args += map(str, args)
+        self.tool.executive.run_and_throw_if_fail(bugzilla_tool_args)
+
+    def log_progress(self, patch_ids):
+        log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
+
+    def execute(self, options, args, tool, engine=QueueEngine):
+        self.options = options
+        self.tool = tool
+        return engine(self.name, self).run()
+
+    @classmethod
+    def _update_status_for_script_error(cls, tool, state, script_error):
+        return tool.status_bot.update_status(cls.name, script_error.message, state["patch"], StringIO(script_error.output))
+
+
+class CommitQueue(AbstractQueue, StepSequenceErrorHandler):
+    name = "commit-queue"
+    def __init__(self):
+        AbstractQueue.__init__(self)
+
+    # AbstractQueue methods
+
+    def begin_work_queue(self):
+        AbstractQueue.begin_work_queue(self)
+
+    def next_work_item(self):
+        patches = self.tool.bugs.queries.fetch_patches_from_commit_queue(reject_invalid_patches=True)
+        if not patches:
+            self._update_status("Empty queue")
+            return None
+        # Only bother logging if we have patches in the queue.
+        self.log_progress([patch['id'] for patch in patches])
+        return patches[0]
+
+    def should_proceed_with_work_item(self, patch):
+        red_builders_names = self.tool.buildbot.red_core_builders_names()
+        if red_builders_names:
+            red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
+            self._update_status("Builders [%s] are red. See http://build.webkit.org" % ", ".join(red_builders_names), None)
+            return False
+        self._update_status("Landing patch", patch)
+        return True
+
+    def process_work_item(self, patch):
+        try:
+            self._cc_watchers(patch["bug_id"])
+            self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--build-style=both", "--quiet", patch["id"]])
+            self._did_pass(patch)
+        except ScriptError, e:
+            self._did_fail(patch)
+            raise e
+
+    def handle_unexpected_error(self, patch, message):
+        self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
+
+    # StepSequenceErrorHandler methods
+
+    @staticmethod
+    def _error_message_for_bug(tool, status_id, script_error):
+        if not script_error.output:
+            return script_error.message_with_output()
+        results_link = tool.status_bot.results_url_for_status(status_id)
+        return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        status_id = cls._update_status_for_script_error(tool, state, script_error)
+        tool.bugs.reject_patch_from_commit_queue(state["patch"]["id"], cls._error_message_for_bug(tool, status_id, script_error))
+
+
+class AbstractReviewQueue(AbstractQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler):
+    def __init__(self, options=None):
+        AbstractQueue.__init__(self, options)
+
+    # PersistentPatchCollectionDelegate methods
+
+    def collection_name(self):
+        return self.name
+
+    def fetch_potential_patch_ids(self):
+        return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue()
+
+    def status_server(self):
+        return self.tool.status_bot
+
+    # AbstractQueue methods
+
+    def begin_work_queue(self):
+        AbstractQueue.begin_work_queue(self)
+        self._patches = PersistentPatchCollection(self)
+
+    def next_work_item(self):
+        patch_id = self._patches.next()
+        if patch_id:
+            return self.tool.bugs.fetch_attachment(patch_id)
+        self._update_status("Empty queue")
+
+    def should_proceed_with_work_item(self, patch):
+        raise NotImplementedError, "subclasses must implement"
+
+    def process_work_item(self, patch):
+        raise NotImplementedError, "subclasses must implement"
+
+    def handle_unexpected_error(self, patch, message):
+        log(message)
+
+    # StepSequenceErrorHandler methods
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        log(script_error.message_with_output())
+
+
+class StyleQueue(AbstractReviewQueue):
+    name = "style-queue"
+    def __init__(self):
+        AbstractReviewQueue.__init__(self)
+
+    def should_proceed_with_work_item(self, patch):
+        self._update_status("Checking style", patch)
+        return True
+
+    def process_work_item(self, patch):
+        try:
+            self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
+            message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
+            self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
+            self._did_pass(patch)
+        except ScriptError, e:
+            self._did_fail(patch)
+            raise e
+
+    @classmethod
+    def handle_script_error(cls, tool, state, script_error):
+        status_id = cls._update_status_for_script_error(tool, state, script_error)
+        if not script_error.command_name() == "check-webkit-style":
+            return
+        message = "Attachment %s did not pass %s:\n\n%s" % (state["patch"]["id"], cls.name, script_error.message_with_output(output_limit=3*1024))
+        tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queues_unittest.py b/WebKitTools/Scripts/webkitpy/commands/queues_unittest.py
new file mode 100644
index 0000000..b985254
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queues_unittest.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import unittest
+
+from webkitpy.commands.commandtest import CommandsTest
+from webkitpy.commands.queues import *
+from webkitpy.commands.queuestest import QueuesTest
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+
+
+class TestQueue(AbstractQueue):
+    name = "test-queue"
+
+
+class AbstractQueueTest(CommandsTest):
+    def _assert_log_progress_output(self, patch_ids, progress_output):
+        OutputCapture().assert_outputs(self, TestQueue().log_progress, [patch_ids], expected_stderr=progress_output)
+
+    def test_log_progress(self):
+        self._assert_log_progress_output([1,2,3], "3 patches in test-queue [1, 2, 3]\n")
+        self._assert_log_progress_output(["1","2","3"], "3 patches in test-queue [1, 2, 3]\n")
+        self._assert_log_progress_output([1], "1 patch in test-queue [1]\n")
+
+    def _assert_run_bugzilla_tool(self, run_args):
+        queue = TestQueue()
+        tool = MockBugzillaTool()
+        queue.bind_to_tool(tool)
+
+        queue.run_bugzilla_tool(run_args)
+        expected_run_args = ["echo", "--status-host=example.com"] + map(str, run_args)
+        tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
+
+    def test_run_bugzilla_tool(self):
+        self._assert_run_bugzilla_tool([1])
+        self._assert_run_bugzilla_tool(["one", 2])
+
+
+class CommitQueueTest(QueuesTest):
+    def test_style_queue(self):
+        expected_stderr = {
+            "begin_work_queue" : "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % os.getcwd(),
+            "next_work_item" : "2 patches in commit-queue [197, 128]\n",
+        }
+        self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr)
+
+
+class StyleQueueTest(QueuesTest):
+    def test_style_queue(self):
+        expected_stderr = {
+            "begin_work_queue" : "CAUTION: style-queue will discard all local changes in \"%s\"\nRunning WebKit style-queue.\n" % os.getcwd(),
+            "handle_unexpected_error" : "Mock error message\n",
+        }
+        self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queuestest.py b/WebKitTools/Scripts/webkitpy/commands/queuestest.py
new file mode 100644
index 0000000..a525fcc
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queuestest.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.mock import Mock
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+
+
+class MockQueueEngine(object):
+    def __init__(self, name, queue):
+        pass
+
+    def run(self):
+        pass
+
+
+class QueuesTest(unittest.TestCase):
+    mock_work_item = {
+        "id" : 1234,
+        "bug_id" : 345,
+        "attacher_email": "adam at example.com",
+    }
+
+    def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, options=Mock(), tool=MockBugzillaTool()):
+        if not expected_stdout:
+            expected_stdout = {}
+        if not expected_stderr:
+            expected_stderr = {}
+        if not args:
+            args = []
+        if not work_item:
+            work_item = self.mock_work_item
+        options.confirm = False # FIXME: We should have a tool.user that we can mock.
+
+        queue.execute(options, args, tool, engine=MockQueueEngine)
+
+        OutputCapture().assert_outputs(self,
+                queue.queue_log_path,
+                expected_stdout=expected_stdout.get("queue_log_path", ""),
+                expected_stderr=expected_stderr.get("queue_log_path", ""))
+        OutputCapture().assert_outputs(self,
+                queue.work_item_log_path,
+                args=[work_item],
+                expected_stdout=expected_stdout.get("work_item_log_path", ""),
+                expected_stderr=expected_stderr.get("work_item_log_path", ""))
+        OutputCapture().assert_outputs(self,
+                queue.begin_work_queue,
+                expected_stdout=expected_stdout.get("begin_work_queue", ""),
+                expected_stderr=expected_stderr.get("begin_work_queue", ""))
+        OutputCapture().assert_outputs(self,
+                queue.should_continue_work_queue,
+                expected_stdout=expected_stdout.get("should_continue_work_queue", ""), expected_stderr=expected_stderr.get("should_continue_work_queue", ""))
+        OutputCapture().assert_outputs(self,
+                queue.next_work_item,
+                expected_stdout=expected_stdout.get("next_work_item", ""),
+                expected_stderr=expected_stderr.get("next_work_item", ""))
+        OutputCapture().assert_outputs(self,
+                queue.should_proceed_with_work_item,
+                args=[work_item],
+                expected_stdout=expected_stdout.get("should_proceed_with_work_item", ""),
+                expected_stderr=expected_stderr.get("should_proceed_with_work_item", ""))
+        OutputCapture().assert_outputs(self,
+                queue.process_work_item,
+                args=[work_item],
+                expected_stdout=expected_stdout.get("process_work_item", ""),
+                expected_stderr=expected_stderr.get("process_work_item", ""))
+        OutputCapture().assert_outputs(self,
+                queue.handle_unexpected_error,
+                args=[work_item, "Mock error message"],
+                expected_stdout=expected_stdout.get("handle_unexpected_error", ""),
+                expected_stderr=expected_stderr.get("handle_unexpected_error", ""))
diff --git a/WebKitTools/Scripts/webkitpy/commands/upload.py b/WebKitTools/Scripts/webkitpy/commands/upload.py
new file mode 100644
index 0000000..d552901
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/upload.py
@@ -0,0 +1,384 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import StringIO
+import sys
+
+from optparse import make_option
+
+from webkitpy.bugzilla import parse_bug_id
+from webkitpy.buildsteps import PrepareChangeLogStep, EditChangeLogStep, ConfirmDiffStep, CommandOptions, ObsoletePatchesOnBugStep, PostDiffToBugStep, PromptForBugOrTitleStep, CreateBugStep
+from webkitpy.commands.download import AbstractSequencedCommmand
+from webkitpy.comments import bug_comment_from_svn_revision
+from webkitpy.committers import CommitterList
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import error, log
+from webkitpy.multicommandtool import Command, AbstractDeclarativeCommmand
+
+# FIXME: Requires unit test.
+class CommitMessageForCurrentDiff(Command):
+    name = "commit-message"
+    def __init__(self):
+        Command.__init__(self, "Print a commit message suitable for the uncommitted changes")
+
+    def execute(self, options, args, tool):
+        os.chdir(tool.scm().checkout_root)
+        print "%s" % tool.scm().commit_message_for_this_commit().message()
+
+
+class AssignToCommitter(AbstractDeclarativeCommmand):
+    name = "assign-to-committer"
+    help_text = "Assign bug to whoever attached the most recent r+'d patch"
+
+    def _assign_bug_to_last_patch_attacher(self, bug_id):
+        committers = CommitterList()
+        bug = self.tool.bugs.fetch_bug(bug_id)
+        assigned_to_email = bug.assigned_to_email()
+        if assigned_to_email != self.tool.bugs.unassigned_email:
+            log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
+            return
+
+        # FIXME: This should call a reviewed_patches() method on bug instead of re-fetching.
+        reviewed_patches = self.tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+        if not reviewed_patches:
+            log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
+            return
+        latest_patch = reviewed_patches[-1]
+        attacher_email = latest_patch["attacher_email"]
+        committer = committers.committer_by_email(attacher_email)
+        if not committer:
+            log("Attacher %s is not a committer.  Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
+            return
+
+        reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch["id"], committer.full_name)
+        self.tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
+
+    def execute(self, options, args, tool):
+        for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+            self._assign_bug_to_last_patch_attacher(bug_id)
+
+
+class ObsoleteAttachments(AbstractSequencedCommmand):
+    name = "obsolete-attachments"
+    help_text = "Mark all attachments on a bug as obsolete"
+    argument_names = "BUGID"
+    steps = [
+        ObsoletePatchesOnBugStep,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        return { "bug_id" : args[0] }
+
+
+class PostDiff(AbstractSequencedCommmand):
+    name = "post-diff"
+    help_text = "Attach the current working directory diff to a bug as a patch file"
+    argument_names = "[BUGID]"
+    show_in_main_help = True
+    steps = [
+        ConfirmDiffStep,
+        ObsoletePatchesOnBugStep,
+        PostDiffToBugStep,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
+        state = {}
+        bug_id = args and args[0]
+        if not bug_id:
+            state["diff"] = tool.scm().create_patch()
+            bug_id = parse_bug_id(state["diff"])
+        if not bug_id:
+            error("No bug id passed and no bug url found in diff, can't post.")
+        state["bug_id"] = bug_id
+        return state
+
+
+class PrepareDiff(AbstractSequencedCommmand):
+    name = "prepare-diff"
+    help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
+    argument_names = "[BUGID]"
+    steps = [
+        PromptForBugOrTitleStep,
+        CreateBugStep,
+        PrepareChangeLogStep,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        bug_id = args and args[0]
+        return { "bug_id" : bug_id }
+
+
+class CreateReview(AbstractSequencedCommmand):
+    name = "create-review"
+    help_text = "Adds a ChangeLog to the current diff and posts it to a (possibly new) bug"
+    argument_names = "[BUGID]"
+    steps = [
+        PromptForBugOrTitleStep,
+        CreateBugStep,
+        PrepareChangeLogStep,
+        EditChangeLogStep,
+        ConfirmDiffStep,
+        ObsoletePatchesOnBugStep,
+        PostDiffToBugStep,
+    ]
+
+    def _prepare_state(self, options, args, tool):
+        bug_id = args and args[0]
+        return { "bug_id" : bug_id }
+
+
+class EditChangeLog(AbstractSequencedCommmand):
+    name = "edit-changelog"
+    help_text = "Opens modified ChangeLogs in $EDITOR"
+    steps = [
+        EditChangeLogStep,
+    ]
+
+
+class PostCommits(Command):
+    name = "post-commits"
+    show_in_main_help = True
+    def __init__(self):
+        options = [
+            make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+            make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
+            make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
+            CommandOptions.obsolete_patches,
+            CommandOptions.review,
+            CommandOptions.request_commit,
+        ]
+        Command.__init__(self, "Attach a range of local commits to bugs as patch files", "COMMITISH", options=options, requires_local_commits=True)
+
+    def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
+        comment_text = None
+        if (options.add_log_as_comment):
+            comment_text = commit_message.body(lstrip=True)
+            comment_text += "---\n"
+            comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+        return comment_text
+
+    def _diff_file_for_commit(self, tool, commit_id):
+        diff = tool.scm().create_patch_from_local_commit(commit_id)
+        return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
+
+    def execute(self, options, args, tool):
+        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+        if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
+            error("bugzilla-tool does not support attaching %s at once.  Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
+
+        have_obsoleted_patches = set()
+        for commit_id in commit_ids:
+            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+
+            # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
+            bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch_from_local_commit(commit_id))
+            if not bug_id:
+                log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
+                continue
+
+            if options.obsolete_patches and bug_id not in have_obsoleted_patches:
+                state = { "bug_id": bug_id }
+                ObsoletePatchesOnBugStep(tool, options).run(state)
+                have_obsoleted_patches.add(bug_id)
+
+            diff_file = self._diff_file_for_commit(tool, commit_id)
+            description = options.description or commit_message.description(lstrip=True, strip_url=True)
+            comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
+            tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+
+# FIXME: Requires unit test.  Blocking issue: too complex for now.
+class MarkBugFixed(Command):
+    name = "mark-bug-fixed"
+    show_in_main_help = True
+    def __init__(self):
+        options = [
+            make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+            make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
+            make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
+            make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
+        ]
+        Command.__init__(self, "Mark the specified bug as fixed", "[SVN_REVISION]", options=options)
+
+    def _fetch_commit_log(self, tool, svn_revision):
+        if not svn_revision:
+            return tool.scm().last_svn_commit_log()
+        return tool.scm().svn_commit_log(svn_revision)
+
+    def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
+        commit_log = self._fetch_commit_log(tool, svn_revision)
+
+        if not bug_id:
+            bug_id = parse_bug_id(commit_log)
+
+        if not svn_revision:
+            match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
+            if match:
+                svn_revision = match.group('svn_revision')
+
+        if not bug_id or not svn_revision:
+            not_found = []
+            if not bug_id:
+                not_found.append("bug id")
+            if not svn_revision:
+                not_found.append("svn revision")
+            error("Could not find %s on command-line or in %s."
+                  % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
+
+        return (bug_id, svn_revision)
+
+    def _open_bug_in_web_browser(self, tool, bug_id):
+        if sys.platform == "darwin":
+            tool.executive.run_command(["open", tool.bugs.short_bug_url_for_bug_id(bug_id)])
+            return
+        log("WARNING: --open is only supported on Mac OS X.")
+
+    def _prompt_user_for_correctness(self, bug_id, svn_revision):
+        answer = raw_input("Is this correct (y/N)? ")
+        if not re.match("^\s*y(es)?", answer, re.IGNORECASE):
+            exit(1)
+
+    def execute(self, options, args, tool):
+        bug_id = options.bug_id
+
+        svn_revision = args and args[0]
+        if svn_revision:
+            if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
+                svn_revision = svn_revision[1:]
+            if not re.match("^[0-9]+$", svn_revision):
+                error("Invalid svn revision: '%s'" % svn_revision)
+
+        needs_prompt = False
+        if not bug_id or not svn_revision:
+            needs_prompt = True
+            (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
+
+        log("Bug: <%s> %s" % (tool.bugs.short_bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
+        log("Revision: %s" % svn_revision)
+
+        if options.open_bug:
+            self._open_bug_in_web_browser(tool, bug_id)
+
+        if needs_prompt:
+            self._prompt_user_for_correctness(bug_id, svn_revision)
+
+        bug_comment = bug_comment_from_svn_revision(svn_revision)
+        if options.comment:
+            bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
+
+        if options.update_only:
+            log("Adding comment to Bug %s." % bug_id)
+            tool.bugs.post_comment_to_bug(bug_id, bug_comment)
+        else:
+            log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
+            tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
+
+
+# FIXME: Requires unit test.  Blocking issue: too complex for now.
+class CreateBug(Command):
+    name = "create-bug"
+    show_in_main_help = True
+    def __init__(self):
+        options = [
+            CommandOptions.cc,
+            CommandOptions.component,
+            make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
+            make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
+            make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
+        ]
+        Command.__init__(self, "Create a bug from local changes or local commits", "[COMMITISH]", options=options)
+
+    def create_bug_from_commit(self, options, args, tool):
+        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+        if len(commit_ids) > 3:
+            error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
+
+        commit_id = commit_ids[0]
+
+        bug_title = ""
+        comment_text = ""
+        if options.prompt:
+            (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+        else:
+            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+            bug_title = commit_message.description(lstrip=True, strip_url=True)
+            comment_text = commit_message.body(lstrip=True)
+            comment_text += "---\n"
+            comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+
+        diff = tool.scm().create_patch_from_local_commit(commit_id)
+        diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
+        bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+        if bug_id and len(commit_ids) > 1:
+            options.bug_id = bug_id
+            options.obsolete_patches = False
+            # FIXME: We should pass through --no-comment switch as well.
+            PostCommits.execute(self, options, commit_ids[1:], tool)
+
+    def create_bug_from_patch(self, options, args, tool):
+        bug_title = ""
+        comment_text = ""
+        if options.prompt:
+            (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+        else:
+            commit_message = tool.scm().commit_message_for_this_commit()
+            bug_title = commit_message.description(lstrip=True, strip_url=True)
+            comment_text = commit_message.body(lstrip=True)
+
+        diff = tool.scm().create_patch()
+        diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
+        bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+    def prompt_for_bug_title_and_comment(self):
+        bug_title = raw_input("Bug title: ")
+        print "Bug comment (hit ^D on blank line to end):"
+        lines = sys.stdin.readlines()
+        try:
+            sys.stdin.seek(0, os.SEEK_END)
+        except IOError:
+            # Cygwin raises an Illegal Seek (errno 29) exception when the above
+            # seek() call is made. Ignoring it seems to cause no harm.
+            # FIXME: Figure out a way to get avoid the exception in the first
+            # place.
+            pass
+        comment_text = "".join(lines)
+        return (bug_title, comment_text)
+
+    def execute(self, options, args, tool):
+        if len(args):
+            if (not tool.scm().supports_local_commits()):
+                error("Extra arguments not supported; patch is taken from working directory.")
+            self.create_bug_from_commit(options, args, tool)
+        else:
+            self.create_bug_from_patch(options, args, tool)
diff --git a/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py b/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py
new file mode 100644
index 0000000..b7a1c99
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.commands.commandtest import CommandsTest
+from webkitpy.commands.upload import *
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+
+class UploadCommandsTest(CommandsTest):
+    def test_assign_to_committer(self):
+        tool = MockBugzillaTool()
+        expected_stderr = "Bug 75 is already assigned to foo at foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n"
+        self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool)
+        tool.bugs.reassign_bug.assert_called_with(42, "eric at webkit.org", "Attachment 128 was posted by a committer and has review+, assigning to Eric Seidel for commit.")
+
+    def test_obsolete_attachments(self):
+        expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+        self.assert_execute_outputs(ObsoleteAttachments(), [42], expected_stderr=expected_stderr)
+
+    def test_post_diff(self):
+        expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+        self.assert_execute_outputs(PostDiff(), [42], expected_stderr=expected_stderr)
+
+    def test_prepare_diff_with_arg(self):
+        self.assert_execute_outputs(PrepareDiff(), [42])
+
+    def test_prepare_diff(self):
+        self.assert_execute_outputs(PrepareDiff(), [])
+
+    def test_create_review(self):
+        expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+        self.assert_execute_outputs(CreateReview(), [42], expected_stderr=expected_stderr)
+
+    def test_edit_changelog(self):
+        self.assert_execute_outputs(EditChangeLog(), [])
diff --git a/WebKitTools/Scripts/webkitpy/comments.py b/WebKitTools/Scripts/webkitpy/comments.py
new file mode 100755
index 0000000..657a373
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/comments.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# A tool for automating dealing with bugzilla, posting patches, committing patches, etc.
+
+from webkitpy.changelogs import view_source_url
+
+def bug_comment_from_svn_revision(svn_revision):
+    return "Committed r%s: <%s>" % (svn_revision, view_source_url(svn_revision))
+
+def bug_comment_from_commit_text(scm, commit_text):
+    svn_revision = scm.svn_revision_from_commit_text(commit_text)
+    return bug_comment_from_svn_revision(svn_revision)
diff --git a/WebKitTools/Scripts/modules/committers.py b/WebKitTools/Scripts/webkitpy/committers.py
similarity index 100%
rename from WebKitTools/Scripts/modules/committers.py
rename to WebKitTools/Scripts/webkitpy/committers.py
diff --git a/WebKitTools/Scripts/modules/committers_unittest.py b/WebKitTools/Scripts/webkitpy/committers_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/committers_unittest.py
rename to WebKitTools/Scripts/webkitpy/committers_unittest.py
diff --git a/WebKitTools/Scripts/modules/cpp_style.py b/WebKitTools/Scripts/webkitpy/cpp_style.py
similarity index 100%
rename from WebKitTools/Scripts/modules/cpp_style.py
rename to WebKitTools/Scripts/webkitpy/cpp_style.py
diff --git a/WebKitTools/Scripts/modules/cpp_style_unittest.py b/WebKitTools/Scripts/webkitpy/cpp_style_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/cpp_style_unittest.py
rename to WebKitTools/Scripts/webkitpy/cpp_style_unittest.py
diff --git a/WebKitTools/Scripts/webkitpy/credentials.py b/WebKitTools/Scripts/webkitpy/credentials.py
new file mode 100644
index 0000000..0a38907
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/credentials.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# Python module for reading stored web credentials from the OS.
+
+import getpass
+import os
+import platform
+import re
+
+from webkitpy.executive import Executive
+from webkitpy.webkit_logging import log
+from webkitpy.scm import Git
+
+class Credentials(object):
+    keychain_entry_not_found = "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain."
+
+    def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd()):
+        self.host = host
+        self.git_prefix = git_prefix
+        self.executive = executive or Executive()
+        self.cwd = cwd
+
+    def _credentials_from_git(self):
+        return [self._read_git_config("username"), self._read_git_config("password")]
+
+    def _read_git_config(self, key):
+        config_key = "%s.%s" % (self.git_prefix, key) if self.git_prefix else key
+        return self.executive.run_command(["git", "config", "--get", config_key], error_handler=Executive.ignore_error).rstrip('\n')
+
+    def _keychain_value_with_label(self, label, source_text):
+        match = re.search("%s\"(?P<value>.+)\"" % label, source_text, re.MULTILINE)
+        if match:
+            return match.group('value')
+
+    def _is_mac_os_x(self):
+        return platform.mac_ver()[0]
+
+    def _parse_security_tool_output(self, security_output):
+        if security_output == self.keychain_entry_not_found:
+            return [None, None]
+        username = self._keychain_value_with_label("^\s*\"acct\"<blob>=", security_output)
+        password = self._keychain_value_with_label("^password: ", security_output)
+        return [username, password]
+
+    def _run_security_tool(self, username=None):
+        security_command = ["/usr/bin/security", "find-internet-password", "-g", "-s", self.host]
+        if username:
+            security_command += ["-a", username]
+
+        log("Reading Keychain for %s account and password.  Click \"Allow\" to continue..." % self.host)
+        return self.executive.run_command(security_command)
+
+    def _credentials_from_keychain(self, username=None):
+        if not self._is_mac_os_x():
+            return [username, None]
+
+        security_output = self._run_security_tool(username)
+        return self._parse_security_tool_output(security_output)
+
+    def read_credentials(self):
+        username = None
+        password = None
+
+        if Git.in_working_directory(self.cwd):
+            (username, password) = self._credentials_from_git()
+
+        if not username or not password:
+            (username, password) = self._credentials_from_keychain(username)
+
+        if not username:
+            username = raw_input("%s login: " % self.host)
+        if not password:
+            password = getpass.getpass("%s password for %s: " % (self.host, username))
+
+        return [username, password]
diff --git a/WebKitTools/Scripts/webkitpy/credentials_unittest.py b/WebKitTools/Scripts/webkitpy/credentials_unittest.py
new file mode 100644
index 0000000..ed36c26
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/credentials_unittest.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import tempfile
+import unittest
+from webkitpy.credentials import Credentials
+from webkitpy.executive import Executive
+from webkitpy.outputcapture import OutputCapture
+from webkitpy.mock import Mock
+
+class CredentialsTest(unittest.TestCase):
+    example_security_output = """keychain: "/Users/test/Library/Keychains/login.keychain"
+class: "inet"
+attributes:
+    0x00000007 <blob>="bugs.webkit.org (test at webkit.org)"
+    0x00000008 <blob>=<NULL>
+    "acct"<blob>="test at webkit.org"
+    "atyp"<blob>="form"
+    "cdat"<timedate>=0x32303039303832353233353231365A00  "20090825235216Z\000"
+    "crtr"<uint32>=<NULL>
+    "cusi"<sint32>=<NULL>
+    "desc"<blob>="Web form password"
+    "icmt"<blob>="default"
+    "invi"<sint32>=<NULL>
+    "mdat"<timedate>=0x32303039303930393137323635315A00  "20090909172651Z\000"
+    "nega"<sint32>=<NULL>
+    "path"<blob>=<NULL>
+    "port"<uint32>=0x00000000 
+    "prot"<blob>=<NULL>
+    "ptcl"<uint32>="htps"
+    "scrp"<sint32>=<NULL>
+    "sdmn"<blob>=<NULL>
+    "srvr"<blob>="bugs.webkit.org"
+    "type"<uint32>=<NULL>
+password: "SECRETSAUCE"
+"""
+
+    def test_keychain_lookup_on_non_mac(self):
+        class FakeCredentials(Credentials):
+            def _is_mac_os_x(self):
+                return False
+        credentials = FakeCredentials("bugs.webkit.org")
+        self.assertEqual(credentials._is_mac_os_x(), False)
+        self.assertEqual(credentials._credentials_from_keychain("foo"), ["foo", None])
+
+    def test_security_output_parse(self):
+        credentials = Credentials("bugs.webkit.org")
+        self.assertEqual(credentials._parse_security_tool_output(self.example_security_output), ["test at webkit.org", "SECRETSAUCE"])
+
+    def test_security_output_parse_entry_not_found(self):
+        credentials = Credentials("foo.example.com")
+        self.assertEqual(credentials._parse_security_tool_output(Credentials.keychain_entry_not_found), [None, None])
+
+    def _assert_security_call(self, username=None):
+        executive_mock = Mock()
+        credentials = Credentials("example.com", executive=executive_mock)
+
+        expected_stderr = "Reading Keychain for example.com account and password.  Click \"Allow\" to continue...\n"
+        OutputCapture().assert_outputs(self, credentials._run_security_tool, [username], expected_stderr=expected_stderr)
+
+        security_args = ["/usr/bin/security", "find-internet-password", "-g", "-s", "example.com"]
+        if username:
+            security_args += ["-a", username]
+        executive_mock.run_command.assert_called_with(security_args)
+
+    def test_security_calls(self):
+        self._assert_security_call()
+        self._assert_security_call(username="foo")
+
+    def test_git_config_calls(self):
+        executive_mock = Mock()
+        credentials = Credentials("example.com", executive=executive_mock)
+        credentials._read_git_config("foo")
+        executive_mock.run_command.assert_called_with(["git", "config", "--get", "foo"], error_handler=Executive.ignore_error)
+
+        credentials = Credentials("example.com", git_prefix="test_prefix", executive=executive_mock)
+        credentials._read_git_config("foo")
+        executive_mock.run_command.assert_called_with(["git", "config", "--get", "test_prefix.foo"], error_handler=Executive.ignore_error)
+
+    def test_read_credentials_without_git_repo(self):
+        class FakeCredentials(Credentials):
+            def _is_mac_os_x(self):
+                return True
+            def _credentials_from_keychain(self, username):
+                return ["test at webkit.org", "SECRETSAUCE"]
+
+        temp_dir_path = tempfile.mkdtemp(suffix="not_a_git_repo")
+        credentials = FakeCredentials("bugs.webkit.org", cwd=temp_dir_path)
+        self.assertEqual(credentials.read_credentials(), ["test at webkit.org", "SECRETSAUCE"])
+        os.rmdir(temp_dir_path)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/modules/diff_parser.py b/WebKitTools/Scripts/webkitpy/diff_parser.py
similarity index 100%
rename from WebKitTools/Scripts/modules/diff_parser.py
rename to WebKitTools/Scripts/webkitpy/diff_parser.py
diff --git a/WebKitTools/Scripts/modules/diff_parser_unittest.py b/WebKitTools/Scripts/webkitpy/diff_parser_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/diff_parser_unittest.py
rename to WebKitTools/Scripts/webkitpy/diff_parser_unittest.py
diff --git a/WebKitTools/Scripts/webkitpy/executive.py b/WebKitTools/Scripts/webkitpy/executive.py
new file mode 100644
index 0000000..dd21489
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/executive.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import StringIO
+import subprocess
+import sys
+
+from webkitpy.webkit_logging import tee
+
+
+class ScriptError(Exception):
+    def __init__(self, message=None, script_args=None, exit_code=None, output=None, cwd=None):
+        if not message:
+            message = 'Failed to run "%s"' % script_args
+            if exit_code:
+                message += " exit_code: %d" % exit_code
+            if cwd:
+                message += " cwd: %s" % cwd
+
+        Exception.__init__(self, message)
+        self.script_args = script_args # 'args' is already used by Exception
+        self.exit_code = exit_code
+        self.output = output
+        self.cwd = cwd
+
+    def message_with_output(self, output_limit=500):
+        if self.output:
+            if output_limit and len(self.output) > output_limit:
+                 return "%s\nLast %s characters of output:\n%s" % (self, output_limit, self.output[-output_limit:])
+            return "%s\n%s" % (self, self.output)
+        return str(self)
+
+    def command_name(self):
+        command_path = self.script_args
+        if type(command_path) is list:
+            command_path = command_path[0]
+        return os.path.basename(command_path)
+
+
+# FIXME: This should not be a global static.
+# New code should use Executive.run_command directly instead
+def run_command(*args, **kwargs):
+    return Executive().run_command(*args, **kwargs)
+
+
+class Executive(object):
+    def _run_command_with_teed_output(self, args, teed_output):
+        child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+        # Use our own custom wait loop because Popen ignores a tee'd stderr/stdout.
+        # FIXME: This could be improved not to flatten output to stdout.
+        while True:
+            output_line = child_process.stdout.readline()
+            if output_line == "" and child_process.poll() != None:
+                return child_process.poll()
+            teed_output.write(output_line)
+
+    def run_and_throw_if_fail(self, args, quiet=False):
+        # Cache the child's output locally so it can be used for error reports.
+        child_out_file = StringIO.StringIO()
+        if quiet:
+            dev_null = open(os.devnull, "w")
+        child_stdout = tee(child_out_file, dev_null if quiet else sys.stdout)
+        exit_code = self._run_command_with_teed_output(args, child_stdout)
+        if quiet:
+            dev_null.close()
+
+        child_output = child_out_file.getvalue()
+        child_out_file.close()
+
+        if exit_code:
+            raise ScriptError(script_args=args, exit_code=exit_code, output=child_output)
+
+    # Error handlers do not need to be static methods once all callers are updated to use an Executive object.
+    @staticmethod
+    def default_error_handler(error):
+        raise error
+
+    @staticmethod
+    def ignore_error(error):
+        pass
+
+    # FIXME: This should be merged with run_and_throw_if_fail
+    def run_command(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True):
+        if hasattr(input, 'read'): # Check if the input is a file.
+            stdin = input
+            string_to_communicate = None
+        else:
+            stdin = subprocess.PIPE if input else None
+            string_to_communicate = input
+        if return_stderr:
+            stderr = subprocess.STDOUT
+        else:
+            stderr = None
+        try:
+            process = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
+            output = process.communicate(string_to_communicate)[0]
+            exit_code = process.wait()
+        except OSError, e:
+            # Catch OSError exceptions. For example, "no such file or directory" (i.e. OSError errno 2),
+            # when the command cannot be found.
+            output = e.strerror
+            exit_code = e.errno
+        if exit_code:
+            script_error = ScriptError(script_args=args, exit_code=exit_code, output=output, cwd=cwd)
+            (error_handler or self.default_error_handler)(script_error)
+        if return_exit_code:
+            return exit_code
+        return output
diff --git a/WebKitTools/Scripts/webkitpy/executive_unittest.py b/WebKitTools/Scripts/webkitpy/executive_unittest.py
new file mode 100644
index 0000000..6ab0d10
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/executive_unittest.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Daniel Bates (dbates at intudata.com). All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+from webkitpy.executive import Executive, ScriptError, run_command
+
+class ExecutiveTest(unittest.TestCase):
+
+    def test_run_command_with_bad_command_check_return_code(self):
+        self.assertEqual(run_command(["foo_bar_command_blah"], error_handler=Executive.ignore_error, return_exit_code=True), 2)
+
+    def test_run_command_with_bad_command_check_calls_error_handler(self):
+        self.didHandleErrorGetCalled = False
+        def handleError(scriptError):
+            self.didHandleErrorGetCalled = True
+            self.assertEqual(scriptError.exit_code, 2)
+
+        run_command(["foo_bar_command_blah"], error_handler=handleError)
+        self.assertTrue(self.didHandleErrorGetCalled)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/modules/grammar.py b/WebKitTools/Scripts/webkitpy/grammar.py
similarity index 100%
rename from WebKitTools/Scripts/modules/grammar.py
rename to WebKitTools/Scripts/webkitpy/grammar.py
diff --git a/WebKitTools/Scripts/modules/mock.py b/WebKitTools/Scripts/webkitpy/mock.py
similarity index 100%
rename from WebKitTools/Scripts/modules/mock.py
rename to WebKitTools/Scripts/webkitpy/mock.py
diff --git a/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py b/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py
new file mode 100644
index 0000000..d570d40
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py
@@ -0,0 +1,214 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from webkitpy.mock import Mock
+from webkitpy.scm import CommitMessage
+from webkitpy.bugzilla import Bug
+
+def _id_to_object_dictionary(*objects):
+    dictionary = {}
+    for thing in objects:
+        dictionary[thing["id"]] = thing
+    return dictionary
+
+# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
+_patch1 = {
+    "id" : 197,
+    "bug_id" : 42,
+    "url" : "http://example.com/197",
+    "is_obsolete" : False,
+    "is_patch" : True,
+    "reviewer" : "Reviewer1",
+    "attacher_email" : "Contributer1",
+}
+_patch2 = {
+    "id" : 128,
+    "bug_id" : 42,
+    "url" : "http://example.com/128",
+    "is_obsolete" : False,
+    "is_patch" : True,
+    "reviewer" : "Reviewer2",
+    "attacher_email" : "eric at webkit.org",
+}
+
+# This must be defined before we define the bugs, thus we don't use MockBugzilla.unassigned_email directly.
+_unassigned_email = "unassigned at example.com"
+
+# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
+_bug1 = {
+    "id" : 42,
+    "assigned_to_email" : _unassigned_email,
+    "attachments" : [_patch1, _patch2],
+}
+_bug2 = {
+    "id" : 75,
+    "assigned_to_email" : "foo at foo.com",
+    "attachments" : [],
+}
+_bug3 = {
+    "id" : 76,
+    "assigned_to_email" : _unassigned_email,
+    "attachments" : [],
+}
+
+class MockBugzillaQueries(Mock):
+    def fetch_bug_ids_from_commit_queue(self):
+        return [42, 75]
+
+    def fetch_attachment_ids_from_review_queue(self):
+        return [197, 128]
+
+    def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
+        return [_patch1, _patch2]
+
+    def fetch_bug_ids_from_pending_commit_list(self):
+        return [42, 75, 76]
+    
+    def fetch_patches_from_pending_commit_list(self):
+        return [_patch1, _patch2]
+
+
+class MockBugzilla(Mock):
+    bug_server_url = "http://example.com"
+    unassigned_email = _unassigned_email
+    bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3)
+    attachment_cache = _id_to_object_dictionary(_patch1, _patch2)
+    queries = MockBugzillaQueries()
+
+    def fetch_bug(self, bug_id):
+        return Bug(self.bug_cache.get(bug_id))
+
+    def fetch_reviewed_patches_from_bug(self, bug_id):
+        return self.fetch_patches_from_bug(bug_id) # Return them all for now.
+
+    def fetch_attachment(self, attachment_id):
+        return self.attachment_cache[attachment_id] # This could be changed to .get() if we wish to allow failed lookups.
+
+    # NOTE: Functions below this are direct copies from bugzilla.py
+    def fetch_patches_from_bug(self, bug_id):
+        return self.fetch_bug(bug_id).patches()
+    
+    def bug_url_for_bug_id(self, bug_id):
+        return "%s/%s" % (self.bug_server_url, bug_id)
+
+    def attachment_url_for_id(self, attachment_id, action):
+        action_param = ""
+        if action and action != "view":
+            action_param = "&action=%s" % action
+        return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param)
+
+
+class MockBuildBot(Mock):
+    def builder_statuses(self):
+        return [{
+            "name": "Builder1",
+            "is_green": True
+        }, {
+            "name": "Builder2",
+            "is_green": True
+        }]
+
+    def red_core_builders_names(self):
+        return []
+
+
+class MockSCM(Mock):
+    def __init__(self):
+        Mock.__init__(self)
+        self.checkout_root = os.getcwd()
+
+    def create_patch(self):
+        return "Patch1"
+
+    def commit_ids_from_commitish_arguments(self, args):
+        return ["Commitish1", "Commitish2"]
+
+    def commit_message_for_local_commit(self, commit_id):
+        if commit_id == "Commitish1":
+            return CommitMessage("CommitMessage1\nhttps://bugs.example.org/show_bug.cgi?id=42\n")
+        if commit_id == "Commitish2":
+            return CommitMessage("CommitMessage2\nhttps://bugs.example.org/show_bug.cgi?id=75\n")
+        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+    def create_patch_from_local_commit(self, commit_id):
+        if commit_id == "Commitish1":
+            return "Patch1"
+        if commit_id == "Commitish2":
+            return "Patch2"
+        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+    def diff_for_revision(self, revision):
+        return "DiffForRevision%s\nhttp://bugs.webkit.org/show_bug.cgi?id=12345" % revision
+
+    def modified_changelogs(self):
+        # Ideally we'd return something more interesting here.
+        # The problem is that LandDiff will try to actually read the path from disk!
+        return []
+
+
+class MockUser(object):
+    def prompt(self, message):
+        return "Mock user response"
+
+    def edit(self, files):
+        pass
+
+    def page(self, message):
+        pass
+
+    def confirm(self):
+        return True
+
+
+class MockStatusBot(object):
+    def __init__(self):
+        self.statusbot_host = "example.com"
+
+    def patch_status(self, queue_name, patch_id):
+        return None
+
+    def update_status(self, queue_name, status, patch=None, results_file=None):
+        return 187
+
+
+class MockBugzillaTool():
+    def __init__(self):
+        self.bugs = MockBugzilla()
+        self.buildbot = MockBuildBot()
+        self.executive = Mock()
+        self.user = MockUser()
+        self._scm = MockSCM()
+        self.status_bot = MockStatusBot()
+
+    def scm(self):
+        return self._scm
+
+    def path(self):
+        return "echo"
diff --git a/WebKitTools/Scripts/webkitpy/multicommandtool.py b/WebKitTools/Scripts/webkitpy/multicommandtool.py
new file mode 100644
index 0000000..5f89852
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/multicommandtool.py
@@ -0,0 +1,293 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# MultiCommandTool provides a framework for writing svn-like/git-like tools
+# which are called with the following format:
+# tool-name [global options] command-name [command options]
+
+import sys
+
+from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option
+
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import log
+
+
+class Command(object):
+    name = None
+    show_in_main_help = False
+    def __init__(self, help_text, argument_names=None, options=None, requires_local_commits=False):
+        self.help_text = help_text
+        self.argument_names = argument_names
+        self.required_arguments = self._parse_required_arguments(argument_names)
+        self.options = options
+        self.requires_local_commits = requires_local_commits
+        self.tool = None
+        # option_parser can be overriden by the tool using set_option_parser
+        # This default parser will be used for standalone_help printing.
+        self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
+
+    # This design is slightly awkward, but we need the
+    # the tool to be able to create and modify the option_parser
+    # before it knows what Command to run.
+    def set_option_parser(self, option_parser):
+        self.option_parser = option_parser
+        self._add_options_to_parser()
+
+    def _add_options_to_parser(self):
+        options = self.options or []
+        for option in options:
+            self.option_parser.add_option(option)
+
+    # The tool calls bind_to_tool on each Command after adding it to its list.
+    def bind_to_tool(self, tool):
+        # Command instances can only be bound to one tool at a time.
+        if self.tool and tool != self.tool:
+            raise Exception("Command already bound to tool!")
+        self.tool = tool
+
+    @staticmethod
+    def _parse_required_arguments(argument_names):
+        required_args = []
+        if not argument_names:
+            return required_args
+        split_args = argument_names.split(" ")
+        for argument in split_args:
+            if argument[0] == '[':
+                # For now our parser is rather dumb.  Do some minimal validation that
+                # we haven't confused it.
+                if argument[-1] != ']':
+                    raise Exception("Failure to parse argument string %s.  Argument %s is missing ending ]" % (argument_names, argument))
+            else:
+                required_args.append(argument)
+        return required_args
+
+    def name_with_arguments(self):
+        usage_string = self.name
+        if self.options:
+            usage_string += " [options]"
+        if self.argument_names:
+            usage_string += " " + self.argument_names
+        return usage_string
+
+    def parse_args(self, args):
+        return self.option_parser.parse_args(args)
+
+    def check_arguments_and_execute(self, options, args, tool=None):
+        if len(args) < len(self.required_arguments):
+            log("%s required, %s provided.  Provided: %s  Required: %s\nSee '%s help %s' for usage." % (
+                pluralize("argument", len(self.required_arguments)),
+                pluralize("argument", len(args)),
+                "'%s'" % " ".join(args),
+                " ".join(self.required_arguments),
+                tool.name(),
+                self.name))
+            return 1
+        return self.execute(options, args, tool) or 0
+
+    def standalone_help(self):
+        help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n"
+        help_text += self.option_parser.format_option_help(IndentedHelpFormatter())
+        return help_text
+
+    def execute(self, options, args, tool):
+        raise NotImplementedError, "subclasses must implement"
+
+    # main() exists so that Commands can be turned into stand-alone scripts.
+    # Other parts of the code will likely require modification to work stand-alone.
+    def main(self, args=sys.argv):
+        (options, args) = self.parse_args(args)
+        # Some commands might require a dummy tool
+        return self.check_arguments_and_execute(options, args)
+
+
+# FIXME: This should just be rolled into Command.  help_text and argument_names do not need to be instance variables.
+class AbstractDeclarativeCommmand(Command):
+    help_text = None
+    argument_names = None
+    def __init__(self, options=None):
+        Command.__init__(self, self.help_text, self.argument_names, options)
+
+
+class HelpPrintingOptionParser(OptionParser):
+    def __init__(self, epilog_method=None, *args, **kwargs):
+        self.epilog_method = epilog_method
+        OptionParser.__init__(self, *args, **kwargs)
+
+    def error(self, msg):
+        self.print_usage(sys.stderr)
+        error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
+        # This method is overriden to add this one line to the output:
+        error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name()
+        self.exit(1, error_message)
+
+    # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog
+    # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive).
+    def format_epilog(self, epilog):
+        if self.epilog_method:
+            return "\n%s\n" % self.epilog_method()
+        return ""
+
+
+class HelpCommand(Command):
+    name = "help"
+
+    def __init__(self):
+        options = [
+            make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"),
+        ]
+        Command.__init__(self, "Display information about this program or its subcommands", "[COMMAND]", options=options)
+        self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser.
+
+    def _help_epilog(self):
+        # Only show commands which are relevant to this checkout's SCM system.  Might this be confusing to some users?
+        if self.show_all_commands:
+            epilog = "All %prog commands:\n"
+            relevant_commands = self.tool.commands[:]
+        else:
+            epilog = "Common %prog commands:\n"
+            relevant_commands = filter(self.tool.should_show_in_main_help, self.tool.commands)
+        longest_name_length = max(map(lambda command: len(command.name), relevant_commands))
+        relevant_commands.sort(lambda a, b: cmp(a.name, b.name))
+        command_help_texts = map(lambda command: "   %s   %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands)
+        epilog += "%s\n" % "".join(command_help_texts)
+        epilog += "See '%prog help --all-commands' to list all commands.\n"
+        epilog += "See '%prog help COMMAND' for more information on a specific command.\n"
+        return epilog.replace("%prog", self.tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name().
+
+    # FIXME: This is a hack so that we don't show --all-commands as a global option:
+    def _remove_help_options(self):
+        for option in self.options:
+            self.option_parser.remove_option(option.get_opt_string())
+
+    def execute(self, options, args, tool):
+        if args:
+            command = self.tool.command_by_name(args[0])
+            if command:
+                print command.standalone_help()
+                return 0
+
+        self.show_all_commands = options.show_all_commands
+        self._remove_help_options()
+        self.option_parser.print_help()
+        return 0
+
+
+class MultiCommandTool(object):
+    global_options = None
+
+    def __init__(self, name=None, commands=None):
+        self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name.
+        # Allow the unit tests to disable command auto-discovery.
+        self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
+        self.help_command = self.command_by_name(HelpCommand.name)
+        # Require a help command, even if the manual test list doesn't include one.
+        if not self.help_command:
+            self.help_command = HelpCommand()
+            self.commands.append(self.help_command)
+        for command in self.commands:
+            command.bind_to_tool(self)
+
+    @classmethod
+    def _add_all_subclasses(cls, class_to_crawl, seen_classes):
+        for subclass in class_to_crawl.__subclasses__():
+            if subclass not in seen_classes:
+                seen_classes.add(subclass)
+                cls._add_all_subclasses(subclass, seen_classes)
+
+    @classmethod
+    def _find_all_commands(cls):
+        commands = set()
+        cls._add_all_subclasses(Command, commands)
+        return sorted(commands)
+
+    def name(self):
+        return self._name
+
+    def _create_option_parser(self):
+        usage = "Usage: %prog [options] COMMAND [ARGS]"
+        return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage)
+
+    @staticmethod
+    def _split_command_name_from_args(args):
+        # Assume the first argument which doesn't start with "-" is the command name.
+        command_index = 0
+        for arg in args:
+            if arg[0] != "-":
+                break
+            command_index += 1
+        else:
+            return (None, args[:])
+
+        command = args[command_index]
+        return (command, args[:command_index] + args[command_index + 1:])
+
+    def command_by_name(self, command_name):
+        for command in self.commands:
+            if command_name == command.name:
+                return command
+        return None
+
+    def path(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def should_show_in_main_help(self, command):
+        return command.show_in_main_help
+
+    def should_execute_command(self, command):
+        return True
+
+    def _add_global_options(self, option_parser):
+        global_options = self.global_options or []
+        for option in global_options:
+            option_parser.add_option(option)
+
+    def handle_global_options(self, options):
+        pass
+
+    def main(self, argv=sys.argv):
+        (command_name, args) = self._split_command_name_from_args(argv[1:])
+
+        option_parser = self._create_option_parser()
+        self._add_global_options(option_parser)
+
+        command = self.command_by_name(command_name) or self.help_command
+        if not command:
+            option_parser.error("%s is not a recognized command" % command_name)
+
+        command.set_option_parser(option_parser)
+        (options, args) = command.parse_args(args)
+        self.handle_global_options(options)
+
+        (should_execute, failure_reason) = self.should_execute_command(command)
+        if not should_execute:
+            log(failure_reason)
+            return 0 # FIXME: Should this really be 0?
+
+        return command.check_arguments_and_execute(options, args, self)
diff --git a/WebKitTools/Scripts/webkitpy/multicommandtool_unittest.py b/WebKitTools/Scripts/webkitpy/multicommandtool_unittest.py
new file mode 100644
index 0000000..ea99507
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/multicommandtool_unittest.py
@@ -0,0 +1,153 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import sys
+import unittest
+from multicommandtool import MultiCommandTool, Command
+from webkitpy.outputcapture import OutputCapture
+
+from optparse import make_option
+
+class TrivialCommand(Command):
+    name = "trivial"
+    show_in_main_help = True
+    def __init__(self, **kwargs):
+        Command.__init__(self, "help text", **kwargs)
+
+    def execute(self, options, args, tool):
+        pass
+
+class UncommonCommand(TrivialCommand):
+    name = "uncommon"
+    show_in_main_help = False
+
+class CommandTest(unittest.TestCase):
+    def test_name_with_arguments(self):
+        command_with_args = TrivialCommand(argument_names="ARG1 ARG2")
+        self.assertEqual(command_with_args.name_with_arguments(), "trivial ARG1 ARG2")
+
+        command_with_args = TrivialCommand(options=[make_option("--my_option")])
+        self.assertEqual(command_with_args.name_with_arguments(), "trivial [options]")
+
+    def test_parse_required_arguments(self):
+        self.assertEqual(Command._parse_required_arguments("ARG1 ARG2"), ["ARG1", "ARG2"])
+        self.assertEqual(Command._parse_required_arguments("[ARG1] [ARG2]"), [])
+        self.assertEqual(Command._parse_required_arguments("[ARG1] ARG2"), ["ARG2"])
+        # Note: We might make our arg parsing smarter in the future and allow this type of arguments string.
+        self.assertRaises(Exception, Command._parse_required_arguments, "[ARG1 ARG2]")
+
+    def test_required_arguments(self):
+        two_required_arguments = TrivialCommand(argument_names="ARG1 ARG2 [ARG3]")
+        expected_missing_args_error = "2 arguments required, 1 argument provided.  Provided: 'foo'  Required: ARG1 ARG2\nSee 'trivial-tool help trivial' for usage.\n"
+        exit_code = OutputCapture().assert_outputs(self, two_required_arguments.check_arguments_and_execute, [None, ["foo"], TrivialTool()], expected_stderr=expected_missing_args_error)
+        self.assertEqual(exit_code, 1)
+
+
+class TrivialTool(MultiCommandTool):
+    def __init__(self, commands=None):
+        MultiCommandTool.__init__(self, name="trivial-tool", commands=commands)
+
+    def path():
+        return __file__
+
+    def should_execute_command(self, command):
+        return (True, None)
+
+
+class MultiCommandToolTest(unittest.TestCase):
+    def _assert_split(self, args, expected_split):
+        self.assertEqual(MultiCommandTool._split_command_name_from_args(args), expected_split)
+
+    def test_split_args(self):
+        # MultiCommandToolTest._split_command_name_from_args returns: (command, args)
+        full_args = ["--global-option", "command", "--option", "arg"]
+        full_args_expected = ("command", ["--global-option", "--option", "arg"])
+        self._assert_split(full_args, full_args_expected)
+
+        full_args = []
+        full_args_expected = (None, [])
+        self._assert_split(full_args, full_args_expected)
+
+        full_args = ["command", "arg"]
+        full_args_expected = ("command", ["arg"])
+        self._assert_split(full_args, full_args_expected)
+
+    def test_command_by_name(self):
+        # This also tests Command auto-discovery.
+        tool = TrivialTool()
+        self.assertEqual(tool.command_by_name("trivial").name, "trivial")
+        self.assertEqual(tool.command_by_name("bar"), None)
+
+    def _assert_tool_main_outputs(self, tool, main_args, expected_stdout, expected_stderr = "", expected_exit_code=0):
+        exit_code = OutputCapture().assert_outputs(self, tool.main, [main_args], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
+        self.assertEqual(exit_code, expected_exit_code)
+
+    def test_global_help(self):
+        tool = TrivialTool(commands=[TrivialCommand(), UncommonCommand()])
+        expected_common_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
+
+Options:
+  -h, --help  show this help message and exit
+
+Common trivial-tool commands:
+   trivial   help text
+
+See 'trivial-tool help --all-commands' to list all commands.
+See 'trivial-tool help COMMAND' for more information on a specific command.
+
+"""
+        self._assert_tool_main_outputs(tool, ["tool"], expected_common_commands_help)
+        self._assert_tool_main_outputs(tool, ["tool", "help"], expected_common_commands_help)
+        expected_all_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
+
+Options:
+  -h, --help  show this help message and exit
+
+All trivial-tool commands:
+   help       Display information about this program or its subcommands
+   trivial    help text
+   uncommon   help text
+
+See 'trivial-tool help --all-commands' to list all commands.
+See 'trivial-tool help COMMAND' for more information on a specific command.
+
+"""
+        self._assert_tool_main_outputs(tool, ["tool", "help", "--all-commands"], expected_all_commands_help)
+        # Test that arguments can be passed before commands as well
+        self._assert_tool_main_outputs(tool, ["tool", "--all-commands", "help"], expected_all_commands_help)
+
+
+    def test_command_help(self):
+        command_with_options = TrivialCommand(options=[make_option("--my_option")])
+        tool = TrivialTool(commands=[command_with_options])
+        expected_subcommand_help = "trivial [options]   help text\nOptions:\n  --my_option=MY_OPTION\n\n"
+        self._assert_tool_main_outputs(tool, ["tool", "help", "trivial"], expected_subcommand_help)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/WebKitTools/Scripts/modules/outputcapture.py b/WebKitTools/Scripts/webkitpy/outputcapture.py
similarity index 100%
rename from WebKitTools/Scripts/modules/outputcapture.py
rename to WebKitTools/Scripts/webkitpy/outputcapture.py
diff --git a/WebKitTools/Scripts/modules/patchcollection.py b/WebKitTools/Scripts/webkitpy/patchcollection.py
similarity index 100%
rename from WebKitTools/Scripts/modules/patchcollection.py
rename to WebKitTools/Scripts/webkitpy/patchcollection.py
diff --git a/WebKitTools/Scripts/webkitpy/queueengine.py b/WebKitTools/Scripts/webkitpy/queueengine.py
new file mode 100644
index 0000000..b234cc1
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/queueengine.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import time
+import traceback
+
+from datetime import datetime, timedelta
+
+from webkitpy.executive import ScriptError
+from webkitpy.webkit_logging import log, OutputTee
+from webkitpy.statusbot import StatusBot
+
+class QueueEngineDelegate:
+    def queue_log_path(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def work_item_log_path(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def begin_work_queue(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def should_continue_work_queue(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def next_work_item(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def should_proceed_with_work_item(self, work_item):
+        # returns (safe_to_proceed, waiting_message, patch)
+        raise NotImplementedError, "subclasses must implement"
+
+    def process_work_item(self, work_item):
+        raise NotImplementedError, "subclasses must implement"
+
+    def handle_unexpected_error(self, work_item, message):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class QueueEngine:
+    def __init__(self, name, delegate):
+        self._name = name
+        self._delegate = delegate
+        self._output_tee = OutputTee()
+
+    log_date_format = "%Y-%m-%d %H:%M:%S"
+    sleep_duration_text = "5 mins"
+    seconds_to_sleep = 300
+    handled_error_code = 2
+
+    # Child processes exit with a special code to the parent queue process can detect the error was handled.
+    @classmethod
+    def exit_after_handled_error(cls, error):
+        log(error)
+        exit(cls.handled_error_code)
+
+    def run(self):
+        self._begin_logging()
+
+        self._delegate.begin_work_queue()
+        while (self._delegate.should_continue_work_queue()):
+            try:
+                self._ensure_work_log_closed()
+                work_item = self._delegate.next_work_item()
+                if not work_item:
+                    self._sleep("No work item.")
+                    continue
+                if not self._delegate.should_proceed_with_work_item(work_item):
+                    self._sleep("Not proceeding with work item.")
+                    continue
+
+                # FIXME: Work logs should not depend on bug_id specificaly.
+                #        This looks fixed, no?
+                self._open_work_log(work_item)
+                try:
+                    self._delegate.process_work_item(work_item)
+                except ScriptError, e:
+                    # Use a special exit code to indicate that the error was already
+                    # handled in the child process and we should just keep looping.
+                    if e.exit_code == self.handled_error_code:
+                        continue
+                    message = "Unexpected failure when landing patch!  Please file a bug against bugzilla-tool.\n%s" % e.message_with_output()
+                    self._delegate.handle_unexpected_error(work_item, message)
+            except KeyboardInterrupt, e:
+                log("\nUser terminated queue.")
+                return 1
+            except Exception, e:
+                traceback.print_exc()
+                # Don't try tell the status bot, in case telling it causes an exception.
+                self._sleep("Exception while preparing queue: %s." % e)
+        # Never reached.
+        self._ensure_work_log_closed()
+
+    def _begin_logging(self):
+        self._queue_log = self._output_tee.add_log(self._delegate.queue_log_path())
+        self._work_log = None
+
+    def _open_work_log(self, work_item):
+        work_item_log_path = self._delegate.work_item_log_path(work_item)
+        self._work_log = self._output_tee.add_log(work_item_log_path)
+
+    def _ensure_work_log_closed(self):
+        # If we still have a bug log open, close it.
+        if self._work_log:
+            self._output_tee.remove_log(self._work_log)
+            self._work_log = None
+
+    @classmethod
+    def _sleep_message(cls, message):
+        wake_time = datetime.now() + timedelta(seconds=cls.seconds_to_sleep)
+        return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(cls.log_date_format), cls.sleep_duration_text)
+
+    @classmethod
+    def _sleep(cls, message):
+        log(cls._sleep_message(message))
+        time.sleep(cls.seconds_to_sleep)
diff --git a/WebKitTools/Scripts/webkitpy/queueengine_unittest.py b/WebKitTools/Scripts/webkitpy/queueengine_unittest.py
new file mode 100644
index 0000000..a4036ea
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/queueengine_unittest.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from webkitpy.executive import ScriptError
+from webkitpy.queueengine import QueueEngine, QueueEngineDelegate
+
+class LoggingDelegate(QueueEngineDelegate):
+    def __init__(self, test):
+        self._test = test
+        self._callbacks = []
+        self._run_before = False
+
+    expected_callbacks = [
+        'queue_log_path',
+        'begin_work_queue',
+        'should_continue_work_queue',
+        'next_work_item',
+        'should_proceed_with_work_item',
+        'work_item_log_path',
+        'process_work_item',
+        'should_continue_work_queue'
+    ]
+
+    def record(self, method_name):
+        self._callbacks.append(method_name)
+
+    def queue_log_path(self):
+        self.record("queue_log_path")
+        return os.path.join(self._test.temp_dir, "queue_log_path")
+
+    def work_item_log_path(self, work_item):
+        self.record("work_item_log_path")
+        return os.path.join(self._test.temp_dir, "work_log_path", "%s.log" % work_item)
+
+    def begin_work_queue(self):
+        self.record("begin_work_queue")
+
+    def should_continue_work_queue(self):
+        self.record("should_continue_work_queue")
+        if not self._run_before:
+            self._run_before = True
+            return True
+        return False
+
+    def next_work_item(self):
+        self.record("next_work_item")
+        return "work_item"
+
+    def should_proceed_with_work_item(self, work_item):
+        self.record("should_proceed_with_work_item")
+        self._test.assertEquals(work_item, "work_item")
+        fake_patch = { 'bug_id' : 42 }
+        return (True, "waiting_message", fake_patch)
+
+    def process_work_item(self, work_item):
+        self.record("process_work_item")
+        self._test.assertEquals(work_item, "work_item")
+
+    def handle_unexpected_error(self, work_item, message):
+        self.record("handle_unexpected_error")
+        self._test.assertEquals(work_item, "work_item")
+
+
+class ThrowErrorDelegate(LoggingDelegate):
+    def __init__(self, test, error_code):
+        LoggingDelegate.__init__(self, test)
+        self.error_code = error_code
+
+    def process_work_item(self, work_item):
+        self.record("process_work_item")
+        raise ScriptError(exit_code=self.error_code)
+
+
+class NotSafeToProceedDelegate(LoggingDelegate):
+    def should_proceed_with_work_item(self, work_item):
+        self.record("should_proceed_with_work_item")
+        self._test.assertEquals(work_item, "work_item")
+        return False
+
+
+class FastQueueEngine(QueueEngine):
+    def __init__(self, delegate):
+        QueueEngine.__init__(self, "fast-queue", delegate)
+
+    # No sleep for the wicked.
+    seconds_to_sleep = 0
+
+    def _sleep(self, message):
+        pass
+
+
+class QueueEngineTest(unittest.TestCase):
+    def test_trivial(self):
+        delegate = LoggingDelegate(self)
+        work_queue = QueueEngine("trivial-queue", delegate)
+        work_queue.run()
+        self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+        self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "queue_log_path")))
+        self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "work_log_path", "work_item.log")))
+
+    def test_unexpected_error(self):
+        delegate = ThrowErrorDelegate(self, 3)
+        work_queue = QueueEngine("error-queue", delegate)
+        work_queue.run()
+        expected_callbacks = LoggingDelegate.expected_callbacks[:]
+        work_item_index = expected_callbacks.index('process_work_item')
+        # The unexpected error should be handled right after process_work_item starts
+        # but before any other callback.  Otherwise callbacks should be normal.
+        expected_callbacks.insert(work_item_index + 1, 'handle_unexpected_error')
+        self.assertEquals(delegate._callbacks, expected_callbacks)
+
+    def test_handled_error(self):
+        delegate = ThrowErrorDelegate(self, QueueEngine.handled_error_code)
+        work_queue = QueueEngine("handled-error-queue", delegate)
+        work_queue.run()
+        self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+
+    def test_not_safe_to_proceed(self):
+        delegate = NotSafeToProceedDelegate(self)
+        work_queue = FastQueueEngine(delegate)
+        work_queue.run()
+        expected_callbacks = LoggingDelegate.expected_callbacks[:]
+        next_work_item_index = expected_callbacks.index('next_work_item')
+        # We slice out the common part of the expected callbacks.
+        # We add 2 here to include should_proceed_with_work_item, which is
+        # a pain to search for directly because it occurs twice.
+        expected_callbacks = expected_callbacks[:next_work_item_index + 2]
+        expected_callbacks.append('should_continue_work_queue')
+        self.assertEquals(delegate._callbacks, expected_callbacks)
+
+    def setUp(self):
+        self.temp_dir = tempfile.mkdtemp(suffix="work_queue_test_logs")
+
+    def tearDown(self):
+        shutil.rmtree(self.temp_dir)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/scm.py b/WebKitTools/Scripts/webkitpy/scm.py
new file mode 100644
index 0000000..9a2fc4a
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/scm.py
@@ -0,0 +1,512 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# Python module for interacting with an SCM system (like SVN or Git)
+
+import os
+import re
+import subprocess
+
+# Import WebKit-specific modules.
+from webkitpy.changelogs import ChangeLog
+from webkitpy.executive import Executive, run_command, ScriptError
+from webkitpy.webkit_logging import error, log
+
+def detect_scm_system(path):
+    if SVN.in_working_directory(path):
+        return SVN(cwd=path)
+    
+    if Git.in_working_directory(path):
+        return Git(cwd=path)
+    
+    return None
+
+def first_non_empty_line_after_index(lines, index=0):
+    first_non_empty_line = index
+    for line in lines[index:]:
+        if re.match("^\s*$", line):
+            first_non_empty_line += 1
+        else:
+            break
+    return first_non_empty_line
+
+
+class CommitMessage:
+    def __init__(self, message):
+        self.message_lines = message[first_non_empty_line_after_index(message, 0):]
+
+    def body(self, lstrip=False):
+        lines = self.message_lines[first_non_empty_line_after_index(self.message_lines, 1):]
+        if lstrip:
+            lines = [line.lstrip() for line in lines]
+        return "\n".join(lines) + "\n"
+
+    def description(self, lstrip=False, strip_url=False):
+        line = self.message_lines[0]
+        if lstrip:
+            line = line.lstrip()
+        if strip_url:
+            line = re.sub("^(\s*)<.+> ", "\1", line)
+        return line
+
+    def message(self):
+        return "\n".join(self.message_lines) + "\n"
+
+
+class CheckoutNeedsUpdate(ScriptError):
+    def __init__(self, script_args, exit_code, output, cwd):
+        ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd)
+
+
+def commit_error_handler(error):
+    if re.search("resource out of date", error.output):
+        raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd)
+    Executive.default_error_handler(error)
+
+
+class SCM:
+    def __init__(self, cwd, dryrun=False):
+        self.cwd = cwd
+        self.checkout_root = self.find_checkout_root(self.cwd)
+        self.dryrun = dryrun
+
+    def scripts_directory(self):
+        return os.path.join(self.checkout_root, "WebKitTools", "Scripts")
+
+    def script_path(self, script_name):
+        return os.path.join(self.scripts_directory(), script_name)
+
+    def ensure_clean_working_directory(self, force_clean):
+        if not force_clean and not self.working_directory_is_clean():
+            print run_command(self.status_command(), error_handler=Executive.ignore_error)
+            raise ScriptError(message="Working directory has modifications, pass --force-clean or --no-clean to continue.")
+        
+        log("Cleaning working directory")
+        self.clean_working_directory()
+    
+    def ensure_no_local_commits(self, force):
+        if not self.supports_local_commits():
+            return
+        commits = self.local_commits()
+        if not len(commits):
+            return
+        if not force:
+            error("Working directory has local commits, pass --force-clean to continue.")
+        self.discard_local_commits()
+
+    def apply_patch(self, patch, force=False):
+        # It's possible that the patch was not made from the root directory.
+        # We should detect and handle that case.
+        curl_process = subprocess.Popen(['curl', '--location', '--silent', '--show-error', patch['url']], stdout=subprocess.PIPE)
+        args = [self.script_path('svn-apply')]
+        if patch.get('reviewer'):
+            args += ['--reviewer', patch['reviewer']]
+        if force:
+            args.append('--force')
+
+        run_command(args, input=curl_process.stdout)
+
+    def run_status_and_extract_filenames(self, status_command, status_regexp):
+        filenames = []
+        for line in run_command(status_command).splitlines():
+            match = re.search(status_regexp, line)
+            if not match:
+                continue
+            # status = match.group('status')
+            filename = match.group('filename')
+            filenames.append(filename)
+        return filenames
+
+    def strip_r_from_svn_revision(self, svn_revision):
+        match = re.match("^r(?P<svn_revision>\d+)", svn_revision)
+        if (match):
+            return match.group('svn_revision')
+        return svn_revision
+
+    def svn_revision_from_commit_text(self, commit_text):
+        match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE)
+        return match.group('svn_revision')
+
+    # ChangeLog-specific code doesn't really belong in scm.py, but this function is very useful.
+    def modified_changelogs(self):
+        changelog_paths = []
+        paths = self.changed_files()
+        for path in paths:
+            if os.path.basename(path) == "ChangeLog":
+                changelog_paths.append(path)
+        return changelog_paths
+
+    # FIXME: Requires unit test
+    # FIXME: commit_message_for_this_commit and modified_changelogs don't
+    #        really belong here.  We should have a separate module for
+    #        handling ChangeLogs.
+    def commit_message_for_this_commit(self):
+        changelog_paths = self.modified_changelogs()
+        if not len(changelog_paths):
+            raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
+                              "All changes require a ChangeLog.  See:\n"
+                              "http://webkit.org/coding/contributing.html")
+
+        changelog_messages = []
+        for changelog_path in changelog_paths:
+            log("Parsing ChangeLog: %s" % changelog_path)
+            changelog_entry = ChangeLog(changelog_path).latest_entry()
+            if not changelog_entry:
+                raise ScriptError(message="Failed to parse ChangeLog: " + os.path.abspath(changelog_path))
+            changelog_messages.append(changelog_entry)
+
+        # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
+        return CommitMessage("".join(changelog_messages).splitlines())
+
+    @staticmethod
+    def in_working_directory(path):
+        raise NotImplementedError, "subclasses must implement"
+
+    @staticmethod
+    def find_checkout_root(path):
+        raise NotImplementedError, "subclasses must implement"
+
+    @staticmethod
+    def commit_success_regexp():
+        raise NotImplementedError, "subclasses must implement"
+
+    def working_directory_is_clean(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def clean_working_directory(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def status_command(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def changed_files(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def display_name(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def create_patch(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    def diff_for_revision(self, revision):
+        raise NotImplementedError, "subclasses must implement"
+
+    def apply_reverse_diff(self, revision):
+        raise NotImplementedError, "subclasses must implement"
+
+    def revert_files(self, file_paths):
+        raise NotImplementedError, "subclasses must implement"
+
+    def commit_with_message(self, message):
+        raise NotImplementedError, "subclasses must implement"
+
+    def svn_commit_log(self, svn_revision):
+        raise NotImplementedError, "subclasses must implement"
+
+    def last_svn_commit_log(self):
+        raise NotImplementedError, "subclasses must implement"
+
+    # Subclasses must indicate if they support local commits,
+    # but the SCM baseclass will only call local_commits methods when this is true.
+    @staticmethod
+    def supports_local_commits():
+        raise NotImplementedError, "subclasses must implement"
+
+    def create_patch_from_local_commit(self, commit_id):
+        error("Your source control manager does not support creating a patch from a local commit.")
+
+    def create_patch_since_local_commit(self, commit_id):
+        error("Your source control manager does not support creating a patch from a local commit.")
+
+    def commit_locally_with_message(self, message):
+        error("Your source control manager does not support local commits.")
+
+    def discard_local_commits(self):
+        pass
+
+    def local_commits(self):
+        return []
+
+
+class SVN(SCM):
+    def __init__(self, cwd, dryrun=False):
+        SCM.__init__(self, cwd, dryrun)
+        self.cached_version = None
+    
+    @staticmethod
+    def in_working_directory(path):
+        return os.path.isdir(os.path.join(path, '.svn'))
+    
+    @classmethod
+    def find_uuid(cls, path):
+        if not cls.in_working_directory(path):
+            return None
+        return cls.value_from_svn_info(path, 'Repository UUID')
+
+    @classmethod
+    def value_from_svn_info(cls, path, field_name):
+        svn_info_args = ['svn', 'info', path]
+        info_output = run_command(svn_info_args).rstrip()
+        match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)
+        if not match:
+            raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)
+        return match.group('value')
+
+    @staticmethod
+    def find_checkout_root(path):
+        uuid = SVN.find_uuid(path)
+        # If |path| is not in a working directory, we're supposed to return |path|.
+        if not uuid:
+            return path
+        # Search up the directory hierarchy until we find a different UUID.
+        last_path = None
+        while True:
+            if uuid != SVN.find_uuid(path):
+                return last_path
+            last_path = path
+            (path, last_component) = os.path.split(path)
+            if last_path == path:
+                return None
+
+    @staticmethod
+    def commit_success_regexp():
+        return "^Committed revision (?P<svn_revision>\d+)\.$"
+
+    def svn_version(self):
+        if not self.cached_version:
+            self.cached_version = run_command(['svn', '--version', '--quiet'])
+        
+        return self.cached_version
+
+    def working_directory_is_clean(self):
+        return run_command(['svn', 'diff']) == ""
+
+    def clean_working_directory(self):
+        run_command(['svn', 'revert', '-R', '.'])
+
+    def status_command(self):
+        return ['svn', 'status']
+
+    def changed_files(self):
+        if self.svn_version() > "1.6":
+            status_regexp = "^(?P<status>[ACDMR]).{6} (?P<filename>.+)$"
+        else:
+            status_regexp = "^(?P<status>[ACDMR]).{5} (?P<filename>.+)$"
+        return self.run_status_and_extract_filenames(self.status_command(), status_regexp)
+
+    @staticmethod
+    def supports_local_commits():
+        return False
+
+    def display_name(self):
+        return "svn"
+
+    def create_patch(self):
+        return run_command(self.script_path("svn-create-patch"), cwd=self.checkout_root, return_stderr=False)
+
+    def diff_for_revision(self, revision):
+        return run_command(['svn', 'diff', '-c', str(revision)])
+
+    def _repository_url(self):
+        return self.value_from_svn_info(self.checkout_root, 'URL')
+
+    def apply_reverse_diff(self, revision):
+        # '-c -revision' applies the inverse diff of 'revision'
+        svn_merge_args = ['svn', 'merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()]
+        log("WARNING: svn merge has been known to take more than 10 minutes to complete.  It is recommended you use git for rollouts.")
+        log("Running '%s'" % " ".join(svn_merge_args))
+        run_command(svn_merge_args)
+
+    def revert_files(self, file_paths):
+        run_command(['svn', 'revert'] + file_paths)
+
+    def commit_with_message(self, message):
+        if self.dryrun:
+            # Return a string which looks like a commit so that things which parse this output will succeed.
+            return "Dry run, no commit.\nCommitted revision 0."
+        return run_command(['svn', 'commit', '-m', message], error_handler=commit_error_handler)
+
+    def svn_commit_log(self, svn_revision):
+        svn_revision = self.strip_r_from_svn_revision(str(svn_revision))
+        return run_command(['svn', 'log', '--non-interactive', '--revision', svn_revision]);
+
+    def last_svn_commit_log(self):
+        # BASE is the checkout revision, HEAD is the remote repository revision
+        # http://svnbook.red-bean.com/en/1.0/ch03s03.html
+        return self.svn_commit_log('BASE')
+
+# All git-specific logic should go here.
+class Git(SCM):
+    def __init__(self, cwd, dryrun=False):
+        SCM.__init__(self, cwd, dryrun)
+
+    @classmethod
+    def in_working_directory(cls, path):
+        return run_command(['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true"
+
+    @classmethod
+    def find_checkout_root(cls, path):
+        # "git rev-parse --show-cdup" would be another way to get to the root
+        (checkout_root, dot_git) = os.path.split(run_command(['git', 'rev-parse', '--git-dir'], cwd=path))
+        # If we were using 2.6 # checkout_root = os.path.relpath(checkout_root, path)
+        if not os.path.isabs(checkout_root): # Sometimes git returns relative paths
+            checkout_root = os.path.join(path, checkout_root)
+        return checkout_root
+    
+    @staticmethod
+    def commit_success_regexp():
+        return "^Committed r(?P<svn_revision>\d+)$"
+
+
+    def discard_local_commits(self):
+        run_command(['git', 'reset', '--hard', 'trunk'])
+    
+    def local_commits(self):
+        return run_command(['git', 'log', '--pretty=oneline', 'HEAD...trunk']).splitlines()
+
+    def rebase_in_progress(self):
+        return os.path.exists(os.path.join(self.checkout_root, '.git/rebase-apply'))
+
+    def working_directory_is_clean(self):
+        return run_command(['git', 'diff-index', 'HEAD']) == ""
+
+    def clean_working_directory(self):
+        # Could run git clean here too, but that wouldn't match working_directory_is_clean
+        run_command(['git', 'reset', '--hard', 'HEAD'])
+        # Aborting rebase even though this does not match working_directory_is_clean
+        if self.rebase_in_progress():
+            run_command(['git', 'rebase', '--abort'])
+
+    def status_command(self):
+        return ['git', 'status']
+
+    def changed_files(self):
+        status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', 'HEAD']
+        status_regexp = '^(?P<status>[ADM])\t(?P<filename>.+)$'
+        return self.run_status_and_extract_filenames(status_command, status_regexp)
+    
+    @staticmethod
+    def supports_local_commits():
+        return True
+
+    def display_name(self):
+        return "git"
+
+    def create_patch(self):
+        return run_command(['git', 'diff', '--binary', 'HEAD'])
+
+    @classmethod
+    def git_commit_from_svn_revision(cls, revision):
+        # git svn find-rev always exits 0, even when the revision is not found.
+        return run_command(['git', 'svn', 'find-rev', 'r%s' % revision]).rstrip()
+
+    def diff_for_revision(self, revision):
+        git_commit = self.git_commit_from_svn_revision(revision)
+        return self.create_patch_from_local_commit(git_commit)
+
+    def apply_reverse_diff(self, revision):
+        # Assume the revision is an svn revision.
+        git_commit = self.git_commit_from_svn_revision(revision)
+        if not git_commit:
+            raise ScriptError(message='Failed to find git commit for revision %s, git svn log output: "%s"' % (revision, git_commit))
+
+        # I think this will always fail due to ChangeLogs.
+        # FIXME: We need to detec specific failure conditions and handle them.
+        run_command(['git', 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error)
+
+        # Fix any ChangeLogs if necessary.
+        changelog_paths = self.modified_changelogs()
+        if len(changelog_paths):
+            run_command([self.script_path('resolve-ChangeLogs')] + changelog_paths)
+
+    def revert_files(self, file_paths):
+        run_command(['git', 'checkout', 'HEAD'] + file_paths)
+
+    def commit_with_message(self, message):
+        self.commit_locally_with_message(message)
+        return self.push_local_commits_to_server()
+
+    def svn_commit_log(self, svn_revision):
+        svn_revision = self.strip_r_from_svn_revision(svn_revision)
+        return run_command(['git', 'svn', 'log', '-r', svn_revision])
+
+    def last_svn_commit_log(self):
+        return run_command(['git', 'svn', 'log', '--limit=1'])
+
+    # Git-specific methods:
+
+    def create_patch_from_local_commit(self, commit_id):
+        return run_command(['git', 'diff', '--binary', commit_id + "^.." + commit_id])
+
+    def create_patch_since_local_commit(self, commit_id):
+        return run_command(['git', 'diff', '--binary', commit_id])
+
+    def commit_locally_with_message(self, message):
+        run_command(['git', 'commit', '--all', '-F', '-'], input=message)
+        
+    def push_local_commits_to_server(self):
+        if self.dryrun:
+            # Return a string which looks like a commit so that things which parse this output will succeed.
+            return "Dry run, no remote commit.\nCommitted r0"
+        return run_command(['git', 'svn', 'dcommit'], error_handler=commit_error_handler)
+
+    # This function supports the following argument formats:
+    # no args : rev-list trunk..HEAD
+    # A..B    : rev-list A..B
+    # A...B   : error!
+    # A B     : [A, B]  (different from git diff, which would use "rev-list A..B")
+    def commit_ids_from_commitish_arguments(self, args):
+        if not len(args):
+            # FIXME: trunk is not always the remote branch name, need a way to detect the name.
+            args.append('trunk..HEAD')
+
+        commit_ids = []
+        for commitish in args:
+            if '...' in commitish:
+                raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish)
+            elif '..' in commitish:
+                commit_ids += reversed(run_command(['git', 'rev-list', commitish]).splitlines())
+            else:
+                # Turn single commits or branch or tag names into commit ids.
+                commit_ids += run_command(['git', 'rev-parse', '--revs-only', commitish]).splitlines()
+        return commit_ids
+
+    def commit_message_for_local_commit(self, commit_id):
+        commit_lines = run_command(['git', 'cat-file', 'commit', commit_id]).splitlines()
+
+        # Skip the git headers.
+        first_line_after_headers = 0
+        for line in commit_lines:
+            first_line_after_headers += 1
+            if line == "":
+                break
+        return CommitMessage(commit_lines[first_line_after_headers:])
+
+    def files_changed_summary_for_commit(self, commit_id):
+        return run_command(['git', 'diff-tree', '--shortstat', '--no-commit-id', commit_id])
diff --git a/WebKitTools/Scripts/webkitpy/scm_unittest.py b/WebKitTools/Scripts/webkitpy/scm_unittest.py
new file mode 100644
index 0000000..c68d367
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/scm_unittest.py
@@ -0,0 +1,594 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import base64
+import os
+import os.path
+import re
+import stat
+import subprocess
+import tempfile
+import unittest
+import urllib
+
+from datetime import date
+from webkitpy.executive import Executive, run_command, ScriptError
+from webkitpy.scm import detect_scm_system, SCM, CheckoutNeedsUpdate, commit_error_handler
+
+# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
+# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
+
+# FIXME: This should be unified into one of the executive.py commands!
+def run_silent(args, cwd=None):
+    process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
+    process.communicate() # ignore output
+    exit_code = process.wait()
+    if exit_code:
+        raise ScriptError('Failed to run "%s"  exit_code: %d  cwd: %s' % (args, exit_code, cwd))
+
+def write_into_file_at_path(file_path, contents):
+    file = open(file_path, 'w')
+    file.write(contents)
+    file.close()
+
+def read_from_path(file_path):
+    file = open(file_path, 'r')
+    contents = file.read()
+    file.close()
+    return contents
+
+# Exists to share svn repository creation code between the git and svn tests
+class SVNTestRepository:
+    @staticmethod
+    def _setup_test_commits(test_object):
+        # Add some test commits
+        os.chdir(test_object.svn_checkout_path)
+        test_file = open('test_file', 'w')
+        test_file.write("test1")
+        test_file.flush()
+        
+        run_command(['svn', 'add', 'test_file'])
+        run_command(['svn', 'commit', '--quiet', '--message', 'initial commit'])
+        
+        test_file.write("test2")
+        test_file.flush()
+        
+        run_command(['svn', 'commit', '--quiet', '--message', 'second commit'])
+        
+        test_file.write("test3\n")
+        test_file.flush()
+        
+        run_command(['svn', 'commit', '--quiet', '--message', 'third commit'])
+
+        test_file.write("test4\n")
+        test_file.close()
+
+        run_command(['svn', 'commit', '--quiet', '--message', 'fourth commit'])
+
+        # svn does not seem to update after commit as I would expect.
+        run_command(['svn', 'update'])
+
+    @classmethod
+    def setup(cls, test_object):
+        # Create an test SVN repository
+        test_object.svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
+        test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path # Not sure this will work on windows
+        # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
+        # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
+        run_command(['svnadmin', 'create', '--pre-1.5-compatible', test_object.svn_repo_path])
+
+        # Create a test svn checkout
+        test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
+        run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
+
+        cls._setup_test_commits(test_object)
+
+    @classmethod
+    def tear_down(cls, test_object):
+        run_command(['rm', '-rf', test_object.svn_repo_path])
+        run_command(['rm', '-rf', test_object.svn_checkout_path])
+
+# For testing the SCM baseclass directly.
+class SCMClassTests(unittest.TestCase):
+    def setUp(self):
+        self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
+
+    def tearDown(self):
+        self.dev_null.close()
+
+    def test_run_command_with_pipe(self):
+        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
+        self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
+
+        # Test the non-pipe case too:
+        self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
+
+        command_returns_non_zero = ['/bin/sh', '--invalid-option']
+        # Test when the input pipe process fails.
+        input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
+        self.assertTrue(input_process.poll() != 0)
+        self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
+
+        # Test when the run_command process fails.
+        input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments.
+        self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
+
+    def test_error_handlers(self):
+        git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469"
+        svn_failure_message="""svn: Commit failed (details follow):
+svn: File or directory 'ChangeLog' is out of date; try updating
+svn: resource out of date; try updating
+"""
+        command_does_not_exist = ['does_not_exist', 'invalid_option']
+        self.assertRaises(OSError, run_command, command_does_not_exist)
+        self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
+
+        command_returns_non_zero = ['/bin/sh', '--invalid-option']
+        self.assertRaises(ScriptError, run_command, command_returns_non_zero)
+        # Check if returns error text:
+        self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
+
+        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
+        self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
+        self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
+
+
+# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
+class SCMTest(unittest.TestCase):
+    def _create_patch(self, patch_contents):
+        patch_path = os.path.join(self.svn_checkout_path, 'patch.diff')
+        write_into_file_at_path(patch_path, patch_contents)
+        patch = {}
+        patch['reviewer'] = 'Joe Cool'
+        patch['bug_id'] = '12345'
+        patch['url'] = 'file://%s' % urllib.pathname2url(patch_path)
+        return patch
+
+    def _setup_webkittools_scripts_symlink(self, local_scm):
+        webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
+        webkit_scripts_directory = webkit_scm.scripts_directory()
+        local_scripts_directory = local_scm.scripts_directory()
+        os.mkdir(os.path.dirname(local_scripts_directory))
+        os.symlink(webkit_scripts_directory, local_scripts_directory)
+
+    # Tests which both GitTest and SVNTest should run.
+    # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
+    def _shared_test_commit_with_message(self):
+        write_into_file_at_path('test_file', 'more test content')
+        commit_text = self.scm.commit_with_message('another test commit')
+        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '5')
+
+        self.scm.dryrun = True
+        write_into_file_at_path('test_file', 'still more test content')
+        commit_text = self.scm.commit_with_message('yet another test commit')
+        self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
+
+    def _shared_test_reverse_diff(self):
+        self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
+        # Only test the simple case, as any other will end up with conflict markers.
+        self.scm.apply_reverse_diff('4')
+        self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
+
+    def _shared_test_diff_for_revision(self):
+        # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
+        r3_patch = self.scm.diff_for_revision(3)
+        self.assertTrue(re.search('test3', r3_patch))
+        self.assertFalse(re.search('test4', r3_patch))
+        self.assertTrue(re.search('test2', r3_patch))
+        self.assertTrue(re.search('test2', self.scm.diff_for_revision(2)))
+
+    def _shared_test_svn_apply_git_patch(self):
+        self._setup_webkittools_scripts_symlink(self.scm)
+        git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+new file mode 100644
+index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
+60151690
+GIT binary patch
+literal 512
+zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
+zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
+zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
+zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
+zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
+zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
+zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
+z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
+z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
+ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
+
+literal 0
+HcmV?d00001
+
+"""
+        self.scm.apply_patch(self._create_patch(git_binary_addition))
+        added = read_from_path('fizzbuzz7.gif')
+        self.assertEqual(512, len(added))
+        self.assertTrue(added.startswith('GIF89a'))
+        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
+
+        # The file already exists.
+        self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_addition))
+
+        git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
+GIT binary patch
+literal 7
+OcmYex&reD$;sO8*F9L)B
+
+literal 512
+zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
+zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
+zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
+zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
+zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
+zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
+zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
+z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
+z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
+ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
+
+"""
+        self.scm.apply_patch(self._create_patch(git_binary_modification))
+        modified = read_from_path('fizzbuzz7.gif')
+        self.assertEqual('foobar\n', modified)
+        self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
+
+        # Applying the same modification should fail.
+        self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_modification))
+
+        git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+deleted file mode 100644
+index 323fae0..0000000
+GIT binary patch
+literal 0
+HcmV?d00001
+
+literal 7
+OcmYex&reD$;sO8*F9L)B
+
+"""
+        self.scm.apply_patch(self._create_patch(git_binary_deletion))
+        self.assertFalse(os.path.exists('fizzbuzz7.gif'))
+        self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
+
+        # Cannot delete again.
+        self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_deletion))
+
+
+class SVNTest(SCMTest):
+
+    @staticmethod
+    def _set_date_and_reviewer(changelog_entry):
+        # Joe Cool matches the reviewer set in SCMTest._create_patch
+        changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
+        # svn-apply will update ChangeLog entries with today's date.
+        return changelog_entry.replace('DATE_HERE', date.today().isoformat())
+
+    def test_svn_apply(self):
+        first_entry = """2009-10-26  Eric Seidel  <eric at webkit.org>
+
+        Reviewed by Foo Bar.
+
+        Most awesome change ever.
+
+        * scm_unittest.py:
+"""
+        intermediate_entry = """2009-10-27  Eric Seidel  <eric at webkit.org>
+
+        Reviewed by Baz Bar.
+
+        A more awesomer change yet!
+
+        * scm_unittest.py:
+"""
+        one_line_overlap_patch = """Index: ChangeLog
+===================================================================
+--- ChangeLog	(revision 5)
++++ ChangeLog	(working copy)
+@@ -1,5 +1,13 @@
+ 2009-10-26  Eric Seidel  <eric at webkit.org>
+
++        Reviewed by NOBODY (OOPS!).
++
++        Second most awsome change ever.
++
++        * scm_unittest.py:
++
++2009-10-26  Eric Seidel  <eric at webkit.org>
++
+         Reviewed by Foo Bar.
+
+         Most awesome change ever.
+"""
+        one_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric at webkit.org>
+
+        Reviewed by REVIEWER_HERE.
+
+        Second most awsome change ever.
+
+        * scm_unittest.py:
+"""
+        two_line_overlap_patch = """Index: ChangeLog
+===================================================================
+--- ChangeLog	(revision 5)
++++ ChangeLog	(working copy)
+@@ -2,6 +2,14 @@
+
+         Reviewed by Foo Bar.
+
++        Second most awsome change ever.
++
++        * scm_unittest.py:
++
++2009-10-26  Eric Seidel  <eric at webkit.org>
++
++        Reviewed by Foo Bar.
++
+         Most awesome change ever.
+
+         * scm_unittest.py:
+"""
+        two_line_overlap_entry = """DATE_HERE  Eric Seidel  <eric at webkit.org>
+
+        Reviewed by Foo Bar.
+
+        Second most awsome change ever.
+
+        * scm_unittest.py:
+"""
+        write_into_file_at_path('ChangeLog', first_entry)
+        run_command(['svn', 'add', 'ChangeLog'])
+        run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
+
+        # Patch files were created against just 'first_entry'.
+        # Add a second commit to make svn-apply have to apply the patches with fuzz.
+        changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
+        write_into_file_at_path('ChangeLog', changelog_contents)
+        run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
+
+        self._setup_webkittools_scripts_symlink(self.scm)
+        self.scm.apply_patch(self._create_patch(one_line_overlap_patch))
+        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
+        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
+
+        self.scm.revert_files(['ChangeLog'])
+        self.scm.apply_patch(self._create_patch(two_line_overlap_patch))
+        expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
+        self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
+
+    def setUp(self):
+        SVNTestRepository.setup(self)
+        os.chdir(self.svn_checkout_path)
+        self.scm = detect_scm_system(self.svn_checkout_path)
+
+    def tearDown(self):
+        SVNTestRepository.tear_down(self)
+
+    def test_create_patch_is_full_patch(self):
+        test_dir_path = os.path.join(self.svn_checkout_path, 'test_dir')
+        os.mkdir(test_dir_path)
+        test_file_path = os.path.join(test_dir_path, 'test_file2')
+        write_into_file_at_path(test_file_path, 'test content')
+        run_command(['svn', 'add', 'test_dir'])
+
+        # create_patch depends on 'svn-create-patch', so make a dummy version.
+        scripts_path = os.path.join(self.svn_checkout_path, 'WebKitTools', 'Scripts')
+        os.makedirs(scripts_path)
+        create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
+        write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n.
+        os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
+
+        # Change into our test directory and run the create_patch command.
+        os.chdir(test_dir_path)
+        scm = detect_scm_system(test_dir_path)
+        self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
+        patch_contents = scm.create_patch()
+        # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
+        self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
+
+    def test_detection(self):
+        scm = detect_scm_system(self.svn_checkout_path)
+        self.assertEqual(scm.display_name(), "svn")
+        self.assertEqual(scm.supports_local_commits(), False)
+
+    def test_apply_small_binary_patch(self):
+        patch_contents = """Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+   + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+"""
+        expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
+        self._setup_webkittools_scripts_symlink(self.scm)
+        patch_file = self._create_patch(patch_contents)
+        self.scm.apply_patch(patch_file)
+        actual_contents = read_from_path("test_file.swf")
+        self.assertEqual(actual_contents, expected_contents)
+
+    def test_apply_svn_patch(self):
+        scm = detect_scm_system(self.svn_checkout_path)
+        patch = self._create_patch(run_command(['svn', 'diff', '-r4:3']))
+        self._setup_webkittools_scripts_symlink(scm)
+        scm.apply_patch(patch)
+
+    def test_apply_svn_patch_force(self):
+        scm = detect_scm_system(self.svn_checkout_path)
+        patch = self._create_patch(run_command(['svn', 'diff', '-r2:4']))
+        self._setup_webkittools_scripts_symlink(scm)
+        self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
+
+    def test_commit_logs(self):
+        # Commits have dates and usernames in them, so we can't just direct compare.
+        self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
+        self.assertTrue(re.search('second commit', self.scm.svn_commit_log(2)))
+
+    def test_commit_text_parsing(self):
+        self._shared_test_commit_with_message()
+
+    def test_reverse_diff(self):
+        self._shared_test_reverse_diff()
+
+    def test_diff_for_revision(self):
+        self._shared_test_diff_for_revision()
+
+    def test_svn_apply_git_patch(self):
+        self._shared_test_svn_apply_git_patch()
+
+class GitTest(SCMTest):
+
+    def _setup_git_clone_of_svn_repository(self):
+        self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
+        # --quiet doesn't make git svn silent, so we use run_silent to redirect output
+        run_silent(['git', 'svn', '--quiet', 'clone', self.svn_repo_url, self.git_checkout_path])
+
+    def _tear_down_git_clone_of_svn_repository(self):
+        run_command(['rm', '-rf', self.git_checkout_path])
+
+    def setUp(self):
+        SVNTestRepository.setup(self)
+        self._setup_git_clone_of_svn_repository()
+        os.chdir(self.git_checkout_path)
+        self.scm = detect_scm_system(self.git_checkout_path)
+
+    def tearDown(self):
+        SVNTestRepository.tear_down(self)
+        self._tear_down_git_clone_of_svn_repository()
+
+    def test_detection(self):
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertEqual(scm.display_name(), "git")
+        self.assertEqual(scm.supports_local_commits(), True)
+
+    def test_rebase_in_progress(self):
+        svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
+        write_into_file_at_path(svn_test_file, "svn_checkout")
+        run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
+
+        git_test_file = os.path.join(self.git_checkout_path, 'test_file')
+        write_into_file_at_path(git_test_file, "git_checkout")
+        run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
+
+        # --quiet doesn't make git svn silent, so use run_silent to redirect output
+        self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
+
+        scm = detect_scm_system(self.git_checkout_path)
+        self.assertTrue(scm.rebase_in_progress())
+
+        # Make sure our cleanup works.
+        scm.clean_working_directory()
+        self.assertFalse(scm.rebase_in_progress())
+
+        # Make sure cleanup doesn't throw when no rebase is in progress.
+        scm.clean_working_directory()
+
+    def test_commitish_parsing(self):
+        scm = detect_scm_system(self.git_checkout_path)
+    
+        # Multiple revisions are cherry-picked.
+        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
+        self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
+    
+        # ... is an invalid range specifier
+        self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
+
+    def test_commitish_order(self):
+        scm = detect_scm_system(self.git_checkout_path)
+
+        commit_range = 'HEAD~3..HEAD'
+
+        actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
+        expected_commits = []
+        expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
+
+        self.assertEqual(actual_commits, expected_commits)
+
+    def test_apply_git_patch(self):
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = self._create_patch(run_command(['git', 'diff', 'HEAD..HEAD^']))
+        self._setup_webkittools_scripts_symlink(scm)
+        scm.apply_patch(patch)
+
+    def test_apply_git_patch_force(self):
+        scm = detect_scm_system(self.git_checkout_path)
+        patch = self._create_patch(run_command(['git', 'diff', 'HEAD~2..HEAD']))
+        self._setup_webkittools_scripts_symlink(scm)
+        self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
+
+    def test_commit_text_parsing(self):
+        self._shared_test_commit_with_message()
+
+    def test_reverse_diff(self):
+        self._shared_test_reverse_diff()
+
+    def test_diff_for_revision(self):
+        self._shared_test_diff_for_revision()
+
+    def test_svn_apply_git_patch(self):
+        self._shared_test_svn_apply_git_patch()
+
+    def test_create_binary_patch(self):
+        # Create a git binary patch and check the contents.
+        scm = detect_scm_system(self.git_checkout_path)
+        test_file_name = 'binary_file'
+        test_file_path = os.path.join(self.git_checkout_path, test_file_name)
+        file_contents = ''.join(map(chr, range(256)))
+        write_into_file_at_path(test_file_path, file_contents)
+        run_command(['git', 'add', test_file_name])
+        patch = scm.create_patch()
+        self.assertTrue(re.search(r'\nliteral 0\n', patch))
+        self.assertTrue(re.search(r'\nliteral 256\n', patch))
+
+        # Check if we can apply the created patch.
+        run_command(['git', 'rm', '-f', test_file_name])
+        self._setup_webkittools_scripts_symlink(scm)
+        self.scm.apply_patch(self._create_patch(patch))
+        self.assertEqual(file_contents, read_from_path(test_file_path))
+
+        # Check if we can create a patch from a local commit.
+        write_into_file_at_path(test_file_path, file_contents)
+        run_command(['git', 'add', test_file_name])
+        run_command(['git', 'commit', '-m', 'binary diff'])
+        patch_from_local_commit = scm.create_patch_from_local_commit('HEAD')
+        self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
+        self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
+        patch_since_local_commit = scm.create_patch_since_local_commit('HEAD^1')
+        self.assertTrue(re.search(r'\nliteral 0\n', patch_since_local_commit))
+        self.assertTrue(re.search(r'\nliteral 256\n', patch_since_local_commit))
+        self.assertEqual(patch_from_local_commit, patch_since_local_commit)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/statusbot.py b/WebKitTools/Scripts/webkitpy/statusbot.py
new file mode 100644
index 0000000..00177a5
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/statusbot.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# WebKit's Python module for interacting with the Commit Queue status page.
+
+from webkitpy.webkit_logging import log
+from webkitpy.webkit_mechanize import Browser
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
+# so this import should always succeed.
+from .BeautifulSoup import BeautifulSoup
+
+import urllib2
+
+
+class StatusBot:
+    default_host = "webkit-commit-queue.appspot.com"
+
+    def __init__(self, host=default_host):
+        self.set_host(host)
+        self.browser = Browser()
+
+    def set_host(self, host):
+        self.statusbot_host = host
+        self.statusbot_server_url = "http://%s" % self.statusbot_host
+
+    def results_url_for_status(self, status_id):
+        return "%s/results/%s" % (self.statusbot_server_url, status_id)
+
+    def update_status(self, queue_name, status, patch=None, results_file=None):
+        # During unit testing, statusbot_host is None
+        if not self.statusbot_host:
+            return
+
+        log(status)
+        update_status_url = "%s/update-status" % self.statusbot_server_url
+        self.browser.open(update_status_url)
+        self.browser.select_form(name="update_status")
+        self.browser['queue_name'] = queue_name
+        if patch:
+            if patch.get('bug_id'):
+                self.browser['bug_id'] = str(patch['bug_id'])
+            if patch.get('id'):
+                self.browser['patch_id'] = str(patch['id'])
+        self.browser['status'] = status
+        if results_file:
+            self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file')
+        response = self.browser.submit()
+        return response.read() # This is the id of the newly created status object.
+
+    def patch_status(self, queue_name, patch_id):
+        update_status_url = "%s/patch-status/%s/%s" % (self.statusbot_server_url, queue_name, patch_id)
+        try:
+            return urllib2.urlopen(update_status_url).read()
+        except urllib2.HTTPError, e:
+            if e.code == 404:
+                return None
+            raise e
diff --git a/WebKitTools/Scripts/webkitpy/stepsequence.py b/WebKitTools/Scripts/webkitpy/stepsequence.py
new file mode 100644
index 0000000..b7e544a
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/stepsequence.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.buildsteps import CommandOptions
+from webkitpy.executive import ScriptError
+from webkitpy.webkit_logging import log
+from webkitpy.scm import CheckoutNeedsUpdate
+from webkitpy.queueengine import QueueEngine
+
+
+class StepSequenceErrorHandler():
+    @classmethod
+    def handle_script_error(cls, tool, patch, script_error):
+        raise NotImplementedError, "subclasses must implement"
+
+
+class StepSequence(object):
+    def __init__(self, steps):
+        self._steps = steps or []
+
+    def options(self):
+        collected_options = [
+            CommandOptions.parent_command,
+            CommandOptions.quiet,
+        ]
+        for step in self._steps:
+            collected_options = collected_options + step.options()
+        # Remove duplicates.
+        collected_options = sorted(set(collected_options))
+        return collected_options
+
+    def _run(self, tool, options, state):
+        for step in self._steps:
+            step(tool, options).run(state)
+
+    def run_and_handle_errors(self, tool, options, state=None):
+        if not state:
+            state = {}
+        try:
+            self._run(tool, options, state)
+        except CheckoutNeedsUpdate, e:
+            log("Commit failed because the checkout is out of date.  Please update and try again.")
+            log("You can pass --no-build to skip building/testing after update if you believe the new commits did not affect the results.")
+            QueueEngine.exit_after_handled_error(e)
+        except ScriptError, e:
+            if not options.quiet:
+                log(e.message_with_output())
+            if options.parent_command:
+                command = tool.command_by_name(options.parent_command)
+                command.handle_script_error(tool, state, e)
+            QueueEngine.exit_after_handled_error(e)
diff --git a/WebKitTools/Scripts/modules/style.py b/WebKitTools/Scripts/webkitpy/style.py
similarity index 100%
rename from WebKitTools/Scripts/modules/style.py
rename to WebKitTools/Scripts/webkitpy/style.py
diff --git a/WebKitTools/Scripts/modules/style_unittest.py b/WebKitTools/Scripts/webkitpy/style_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/style_unittest.py
rename to WebKitTools/Scripts/webkitpy/style_unittest.py
diff --git a/WebKitTools/Scripts/modules/text_style.py b/WebKitTools/Scripts/webkitpy/text_style.py
similarity index 100%
rename from WebKitTools/Scripts/modules/text_style.py
rename to WebKitTools/Scripts/webkitpy/text_style.py
diff --git a/WebKitTools/Scripts/modules/text_style_unittest.py b/WebKitTools/Scripts/webkitpy/text_style_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/text_style_unittest.py
rename to WebKitTools/Scripts/webkitpy/text_style_unittest.py
diff --git a/WebKitTools/Scripts/modules/user.py b/WebKitTools/Scripts/webkitpy/user.py
similarity index 100%
rename from WebKitTools/Scripts/modules/user.py
rename to WebKitTools/Scripts/webkitpy/user.py
diff --git a/WebKitTools/Scripts/modules/webkit_logging.py b/WebKitTools/Scripts/webkitpy/webkit_logging.py
similarity index 100%
rename from WebKitTools/Scripts/modules/webkit_logging.py
rename to WebKitTools/Scripts/webkitpy/webkit_logging.py
diff --git a/WebKitTools/Scripts/webkitpy/webkit_logging_unittest.py b/WebKitTools/Scripts/webkitpy/webkit_logging_unittest.py
new file mode 100644
index 0000000..b940a4d
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/webkit_logging_unittest.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import subprocess
+import StringIO
+import tempfile
+import unittest
+
+from webkitpy.executive import ScriptError
+from webkitpy.webkit_logging import *
+
+class LoggingTest(unittest.TestCase):
+
+    def assert_log_equals(self, log_input, expected_output):
+        original_stderr = sys.stderr
+        test_stderr = StringIO.StringIO()
+        sys.stderr = test_stderr
+
+        try:
+            log(log_input)
+            actual_output = test_stderr.getvalue()
+        finally:
+            original_stderr = original_stderr
+
+        self.assertEquals(actual_output, expected_output, "log(\"%s\") expected: %s actual: %s" % (log_input, expected_output, actual_output))
+
+    def test_log(self):
+        self.assert_log_equals("test", "test\n")
+
+        # Test that log() does not throw an exception when passed an object instead of a string.
+        self.assert_log_equals(ScriptError(message="ScriptError"), "ScriptError\n")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/WebKitTools/Scripts/modules/webkit_mechanize.py b/WebKitTools/Scripts/webkitpy/webkit_mechanize.py
similarity index 100%
rename from WebKitTools/Scripts/modules/webkit_mechanize.py
rename to WebKitTools/Scripts/webkitpy/webkit_mechanize.py
diff --git a/WebKitTools/Scripts/modules/webkitport.py b/WebKitTools/Scripts/webkitpy/webkitport.py
similarity index 100%
rename from WebKitTools/Scripts/modules/webkitport.py
rename to WebKitTools/Scripts/webkitpy/webkitport.py
diff --git a/WebKitTools/Scripts/webkitpy/webkitport_unittest.py b/WebKitTools/Scripts/webkitpy/webkitport_unittest.py
new file mode 100644
index 0000000..b699038
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/webkitport_unittest.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# 
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.webkitport import WebKitPort, MacPort, GtkPort, QtPort, ChromiumPort
+
+class WebKitPortTest(unittest.TestCase):
+    def test_mac_port(self):
+        self.assertEquals(MacPort.name(), "Mac")
+        self.assertEquals(MacPort.flag(), "--port=mac")
+        self.assertEquals(MacPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
+        self.assertEquals(MacPort.build_webkit_command(), [WebKitPort.script_path("build-webkit")])
+        self.assertEquals(MacPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug"])
+        self.assertEquals(MacPort.build_webkit_command(build_style="release"), [WebKitPort.script_path("build-webkit"), "--release"])
+
+    def test_gtk_port(self):
+        self.assertEquals(GtkPort.name(), "Gtk")
+        self.assertEquals(GtkPort.flag(), "--port=gtk")
+        self.assertEquals(GtkPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests"), "--gtk"])
+        self.assertEquals(GtkPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--gtk"])
+        self.assertEquals(GtkPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--gtk"])
+
+    def test_qt_port(self):
+        self.assertEquals(QtPort.name(), "Qt")
+        self.assertEquals(QtPort.flag(), "--port=qt")
+        self.assertEquals(QtPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
+        self.assertEquals(QtPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--qt", '--makeargs="-j8"'])
+        self.assertEquals(QtPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--qt", '--makeargs="-j8"'])
+
+    def test_chromium_port(self):
+        self.assertEquals(ChromiumPort.name(), "Chromium")
+        self.assertEquals(ChromiumPort.flag(), "--port=chromium")
+        self.assertEquals(ChromiumPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
+        self.assertEquals(ChromiumPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--chromium"])
+        self.assertEquals(ChromiumPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--chromium"])
+        self.assertEquals(ChromiumPort.update_webkit_command(), [WebKitPort.script_path("update-webkit"), "--chromium"])
+
+
+if __name__ == '__main__':
+    unittest.main()

-- 
WebKit Debian packaging



More information about the Pkg-webkit-commits mailing list