Initial Contribution
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0184e08a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+GERRIT_SRC=../gerrit
+GERRIT_MODULES=codereview froofle
+
+all:
+
+clean:
+	find . -name \*.pyc -type f | xargs rm -f
+
+update-pyclient:
+	$(MAKE) -C $(GERRIT_SRC) release-pyclient
+	rm -rf $(GERRIT_MODULES)
+	(cd $(GERRIT_SRC)/release/pyclient && \
+	 find . -type f \
+	 | cpio -pd $(abspath .))
diff --git a/codereview/__init__.py b/codereview/__init__.py
new file mode 100644
index 0000000..e47bc94
--- /dev/null
+++ b/codereview/__init__.py
@@ -0,0 +1 @@
+__version__ = 'v1.0'
diff --git a/codereview/need_retry_pb2.py b/codereview/need_retry_pb2.py
new file mode 100644
index 0000000..3fab2d4
--- /dev/null
+++ b/codereview/need_retry_pb2.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python2.4
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+
+from froofle.protobuf import descriptor
+from froofle.protobuf import message
+from froofle.protobuf import reflection
+from froofle.protobuf import service
+from froofle.protobuf import service_reflection
+from froofle.protobuf import descriptor_pb2
+
+
+
+_RETRYREQUESTLATERRESPONSE = descriptor.Descriptor(
+  name='RetryRequestLaterResponse',
+  full_name='codereview.RetryRequestLaterResponse',
+  filename='need_retry.proto',
+  containing_type=None,
+  fields=[
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+
+class RetryRequestLaterResponse(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _RETRYREQUESTLATERRESPONSE
+
diff --git a/codereview/proto_client.py b/codereview/proto_client.py
new file mode 100755
index 0000000..e11beff
--- /dev/null
+++ b/codereview/proto_client.py
@@ -0,0 +1,349 @@
+# Copyright 2007, 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import cookielib
+import getpass
+import logging
+import md5
+import os
+import random
+import socket
+import time
+import urllib
+import urllib2
+import urlparse
+
+from froofle.protobuf.service import RpcChannel
+from froofle.protobuf.service import RpcController
+from need_retry_pb2 import RetryRequestLaterResponse;
+
+class ClientLoginError(urllib2.HTTPError):
+  """Raised to indicate an error authenticating with ClientLogin."""
+
+  def __init__(self, url, code, msg, headers, args):
+    urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
+    self.args = args
+    self.reason = args["Error"]
+
+
+class Proxy(object):
+  class _ResultHolder(object):
+    def __call__(self, result):
+      self._result = result
+
+  class _RemoteController(RpcController):
+    def Reset(self):
+      pass
+  
+    def Failed(self):
+      pass
+  
+    def ErrorText(self):
+      pass
+  
+    def StartCancel(self):
+      pass
+  
+    def SetFailed(self, reason):
+      raise RuntimeError, reason
+  
+    def IsCancelled(self):
+      pass
+  
+    def NotifyOnCancel(self, callback):
+      pass
+  
+  def __init__(self, stub):
+    self._stub = stub
+
+  def __getattr__(self, key):
+    method = getattr(self._stub, key)
+
+    def call(request):
+      done = self._ResultHolder()
+      method(self._RemoteController(), request, done)
+      return done._result
+
+    return call
+
+
+class HttpRpc(RpcChannel):
+  """Simple protobuf over HTTP POST implementation."""
+
+  def __init__(self, host, auth_function,
+               host_override=None,
+               extra_headers={},
+               cookie_file=None):
+    """Creates a new HttpRpc.
+
+    Args:
+      host: The host to send requests to.
+      auth_function: A function that takes no arguments and returns an
+        (email, password) tuple when called. Will be called if authentication
+        is required.
+      host_override: The host header to send to the server (defaults to host).
+      extra_headers: A dict of extra headers to append to every request.
+      cookie_file: If not None, name of the file in ~/ to save the
+        cookie jar into.  Applications are encouraged to set this to
+        '.$appname_cookies' or some otherwise unique name.
+    """
+    self.host = host.lower()
+    self.host_override = host_override
+    self.auth_function = auth_function
+    self.authenticated = False
+    self.extra_headers = extra_headers
+    self.xsrf_token = None
+    if cookie_file is None:
+      self.cookie_file = None
+    else:
+      self.cookie_file = os.path.expanduser("~/%s" % cookie_file)
+    self.opener = self._GetOpener()
+    if self.host_override:
+      logging.info("Server: %s; Host: %s", self.host, self.host_override)
+    else:
+      logging.info("Server: %s", self.host)
+
+  def CallMethod(self, method, controller, request, response_type, done):
+    pat = "application/x-google-protobuf; name=%s"
+
+    url = "/proto/%s/%s" % (method.containing_service.name, method.name)
+    reqbin = request.SerializeToString()
+    reqtyp = pat % request.DESCRIPTOR.full_name
+    reqmd5 = base64.b64encode(md5.new(reqbin).digest())
+
+    start = time.time()
+    while True:
+      t, b = self._Send(url, reqbin, reqtyp, reqmd5)
+      if t == (pat % RetryRequestLaterResponse.DESCRIPTOR.full_name):
+        if time.time() >= (start + 1800):
+          controller.SetFailed("timeout")
+          return
+        s = random.uniform(0.250, 2.000)
+        print "Busy, retrying in %.3f seconds ..." % s
+        time.sleep(s)
+        continue
+
+      if t == (pat % response_type.DESCRIPTOR.full_name):
+        response = response_type()
+        response.ParseFromString(b)
+        done(response)
+      else:
+        controller.SetFailed("Unexpected %s response" % t)
+      break
+
+  def _CreateRequest(self, url, data=None):
+    """Creates a new urllib request."""
+    logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
+    req = urllib2.Request(url, data=data)
+    if self.host_override:
+      req.add_header("Host", self.host_override)
+    for key, value in self.extra_headers.iteritems():
+      req.add_header(key, value)
+    return req
+
+  def _GetAuthToken(self, email, password):
+    """Uses ClientLogin to authenticate the user, returning an auth token.
+
+    Args:
+      email:    The user's email address
+      password: The user's password
+
+    Raises:
+      ClientLoginError: If there was an error authenticating with ClientLogin.
+      HTTPError: If there was some other form of HTTP error.
+
+    Returns:
+      The authentication token returned by ClientLogin.
+    """
+    req = self._CreateRequest(
+        url="https://www.google.com/accounts/ClientLogin",
+        data=urllib.urlencode({
+            "Email": email,
+            "Passwd": password,
+            "service": "ah",
+            "source": "gerrit-codereview-client",
+            "accountType": "HOSTED_OR_GOOGLE",
+        })
+    )
+    try:
+      response = self.opener.open(req)
+      response_body = response.read()
+      response_dict = dict(x.split("=")
+                           for x in response_body.split("\n") if x)
+      return response_dict["Auth"]
+    except urllib2.HTTPError, e:
+      if e.code == 403:
+        body = e.read()
+        response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
+        raise ClientLoginError(req.get_full_url(), e.code, e.msg,
+                               e.headers, response_dict)
+      else:
+        raise
+
+  def _GetAuthCookie(self, auth_token):
+    """Fetches authentication cookies for an authentication token.
+
+    Args:
+      auth_token: The authentication token returned by ClientLogin.
+
+    Raises:
+      HTTPError: If there was an error fetching the authentication cookies.
+    """
+    # This is a dummy value to allow us to identify when we're successful.
+    continue_location = "http://localhost/"
+    args = {"continue": continue_location, "auth": auth_token}
+    req = self._CreateRequest("http://%s/_ah/login?%s" %
+                              (self.host, urllib.urlencode(args)))
+    try:
+      response = self.opener.open(req)
+    except urllib2.HTTPError, e:
+      response = e
+    if (response.code != 302 or
+        response.info()["location"] != continue_location):
+      raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
+                              response.headers, response.fp)
+    self.authenticated = True
+
+  def _GetXsrfToken(self):
+    """Fetches /proto/_token for use in X-XSRF-Token HTTP header.
+
+    Raises:
+      HTTPError: If there was an error fetching a new token.
+    """
+    tries = 0
+    while True:
+      url = "http://%s/proto/_token" % self.host
+      req = self._CreateRequest(url)
+      try:
+        response = self.opener.open(req)
+        self.xsrf_token = response.read()
+        return
+      except urllib2.HTTPError, e:
+        if tries > 3:
+          raise
+        elif e.code == 401:
+          self._Authenticate()
+        else:
+          raise
+
+  def _Authenticate(self):
+    """Authenticates the user.
+
+    The authentication process works as follows:
+     1) We get a username and password from the user
+     2) We use ClientLogin to obtain an AUTH token for the user
+        (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
+     3) We pass the auth token to /_ah/login on the server to obtain an
+        authentication cookie. If login was successful, it tries to redirect
+        us to the URL we provided.
+
+    If we attempt to access the upload API without first obtaining an
+    authentication cookie, it returns a 401 response and directs us to
+    authenticate ourselves with ClientLogin.
+    """
+    for i in range(3):
+      credentials = self.auth_function()
+      auth_token = self._GetAuthToken(credentials[0], credentials[1])
+      self._GetAuthCookie(auth_token)
+      if self.cookie_file is not None:
+        self.cookie_jar.save()
+      return
+
+  def _Send(self, request_path, payload, content_type, content_md5):
+    """Sends an RPC and returns the response.
+
+    Args:
+      request_path: The path to send the request to, eg /api/appversion/create.
+      payload: The body of the request, or None to send an empty request.
+      content_type: The Content-Type header to use.
+      content_md5: The Content-MD5 header to use.
+
+    Returns:
+      The content type, as a string.
+      The response body, as a string.
+    """
+    if not self.authenticated:
+      self._Authenticate()
+    if not self.xsrf_token:
+      self._GetXsrfToken()
+
+    old_timeout = socket.getdefaulttimeout()
+    socket.setdefaulttimeout(None)
+    try:
+      tries = 0
+      while True:
+        tries += 1
+        url = "http://%s%s" % (self.host, request_path)
+        req = self._CreateRequest(url=url, data=payload)
+        req.add_header("Content-Type", content_type)
+        req.add_header("Content-MD5", content_md5)
+        req.add_header("X-XSRF-Token", self.xsrf_token)
+        try:
+          f = self.opener.open(req)
+          hdr = f.info()
+          type = hdr.getheader('Content-Type',
+                               'application/octet-stream')
+          response = f.read()
+          f.close()
+          return type, response
+        except urllib2.HTTPError, e:
+          if tries > 3:
+            raise
+          elif e.code == 401:
+            self._Authenticate()
+          elif e.code == 403:
+            if not hasattr(e, 'read'):
+              e.read = lambda self: ''
+            raise RuntimeError, '403\nxsrf: %s\n%s' \
+                  % (self.xsrf_token, e.read())
+          else:
+            raise
+    finally:
+      socket.setdefaulttimeout(old_timeout)
+
+  def _GetOpener(self):
+    """Returns an OpenerDirector that supports cookies and ignores redirects.
+
+    Returns:
+      A urllib2.OpenerDirector object.
+    """
+    opener = urllib2.OpenerDirector()
+    opener.add_handler(urllib2.ProxyHandler())
+    opener.add_handler(urllib2.UnknownHandler())
+    opener.add_handler(urllib2.HTTPHandler())
+    opener.add_handler(urllib2.HTTPDefaultErrorHandler())
+    opener.add_handler(urllib2.HTTPSHandler())
+    opener.add_handler(urllib2.HTTPErrorProcessor())
+    if self.cookie_file is not None:
+      self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
+      if os.path.exists(self.cookie_file):
+        try:
+          self.cookie_jar.load()
+          self.authenticated = True
+        except (cookielib.LoadError, IOError):
+          # Failed to load cookies - just ignore them.
+          pass
+      else:
+        # Create an empty cookie file with mode 600
+        fd = os.open(self.cookie_file, os.O_CREAT, 0600)
+        os.close(fd)
+      # Always chmod the cookie file
+      os.chmod(self.cookie_file, 0600)
+    else:
+      # Don't save cookies across runs of update.py.
+      self.cookie_jar = cookielib.CookieJar()
+    opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
+    return opener
+
diff --git a/codereview/review_pb2.py b/codereview/review_pb2.py
new file mode 100644
index 0000000..0896feb
--- /dev/null
+++ b/codereview/review_pb2.py
@@ -0,0 +1,48 @@
+#!/usr/bin/python2.4
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+
+from froofle.protobuf import descriptor
+from froofle.protobuf import message
+from froofle.protobuf import reflection
+from froofle.protobuf import service
+from froofle.protobuf import service_reflection
+from froofle.protobuf import descriptor_pb2
+
+
+import upload_bundle_pb2
+
+
+
+_REVIEWSERVICE = descriptor.ServiceDescriptor(
+  name='ReviewService',
+  full_name='codereview.ReviewService',
+  index=0,
+  options=None,
+  methods=[
+  descriptor.MethodDescriptor(
+    name='UploadBundle',
+    full_name='codereview.ReviewService.UploadBundle',
+    index=0,
+    containing_service=None,
+    input_type=upload_bundle_pb2._UPLOADBUNDLEREQUEST,
+    output_type=upload_bundle_pb2._UPLOADBUNDLERESPONSE,
+    options=None,
+  ),
+  descriptor.MethodDescriptor(
+    name='ContinueBundle',
+    full_name='codereview.ReviewService.ContinueBundle',
+    index=1,
+    containing_service=None,
+    input_type=upload_bundle_pb2._UPLOADBUNDLECONTINUE,
+    output_type=upload_bundle_pb2._UPLOADBUNDLERESPONSE,
+    options=None,
+  ),
+])
+
+class ReviewService(service.Service):
+  __metaclass__ = service_reflection.GeneratedServiceType
+  DESCRIPTOR = _REVIEWSERVICE
+class ReviewService_Stub(ReviewService):
+  __metaclass__ = service_reflection.GeneratedServiceStubType
+  DESCRIPTOR = _REVIEWSERVICE
+
diff --git a/codereview/upload_bundle_pb2.py b/codereview/upload_bundle_pb2.py
new file mode 100644
index 0000000..48c3651
--- /dev/null
+++ b/codereview/upload_bundle_pb2.py
@@ -0,0 +1,190 @@
+#!/usr/bin/python2.4
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+
+from froofle.protobuf import descriptor
+from froofle.protobuf import message
+from froofle.protobuf import reflection
+from froofle.protobuf import service
+from froofle.protobuf import service_reflection
+from froofle.protobuf import descriptor_pb2
+
+
+_UPLOADBUNDLERESPONSE_CODETYPE = descriptor.EnumDescriptor(
+  name='CodeType',
+  full_name='codereview.UploadBundleResponse.CodeType',
+  filename='CodeType',
+  values=[
+    descriptor.EnumValueDescriptor(
+      name='RECEIVED', index=0, number=1,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='CONTINUE', index=1, number=4,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='UNAUTHORIZED_USER', index=2, number=7,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='UNKNOWN_PROJECT', index=3, number=2,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='UNKNOWN_BRANCH', index=4, number=3,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='UNKNOWN_BUNDLE', index=5, number=5,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='NOT_BUNDLE_OWNER', index=6, number=6,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='BUNDLE_CLOSED', index=7, number=8,
+      options=None,
+      type=None),
+  ],
+  options=None,
+)
+
+
+_UPLOADBUNDLEREQUEST = descriptor.Descriptor(
+  name='UploadBundleRequest',
+  full_name='codereview.UploadBundleRequest',
+  filename='upload_bundle.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='dest_project', full_name='codereview.UploadBundleRequest.dest_project', index=0,
+      number=10, type=9, cpp_type=9, label=2,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='dest_branch', full_name='codereview.UploadBundleRequest.dest_branch', index=1,
+      number=11, type=9, cpp_type=9, label=2,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='partial_upload', full_name='codereview.UploadBundleRequest.partial_upload', index=2,
+      number=12, type=8, cpp_type=7, label=2,
+      default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='bundle_data', full_name='codereview.UploadBundleRequest.bundle_data', index=3,
+      number=13, type=12, cpp_type=9, label=2,
+      default_value="",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='contained_object', full_name='codereview.UploadBundleRequest.contained_object', index=4,
+      number=1, type=9, cpp_type=9, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_UPLOADBUNDLERESPONSE = descriptor.Descriptor(
+  name='UploadBundleResponse',
+  full_name='codereview.UploadBundleResponse',
+  filename='upload_bundle.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='status_code', full_name='codereview.UploadBundleResponse.status_code', index=0,
+      number=10, type=14, cpp_type=8, label=2,
+      default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='bundle_id', full_name='codereview.UploadBundleResponse.bundle_id', index=1,
+      number=11, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+    _UPLOADBUNDLERESPONSE_CODETYPE,
+  ],
+  options=None)
+
+
+_UPLOADBUNDLECONTINUE = descriptor.Descriptor(
+  name='UploadBundleContinue',
+  full_name='codereview.UploadBundleContinue',
+  filename='upload_bundle.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='bundle_id', full_name='codereview.UploadBundleContinue.bundle_id', index=0,
+      number=10, type=9, cpp_type=9, label=2,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='segment_id', full_name='codereview.UploadBundleContinue.segment_id', index=1,
+      number=11, type=5, cpp_type=1, label=2,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='partial_upload', full_name='codereview.UploadBundleContinue.partial_upload', index=2,
+      number=12, type=8, cpp_type=7, label=2,
+      default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='bundle_data', full_name='codereview.UploadBundleContinue.bundle_data', index=3,
+      number=13, type=12, cpp_type=9, label=1,
+      default_value="",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_UPLOADBUNDLERESPONSE.fields_by_name['status_code'].enum_type = _UPLOADBUNDLERESPONSE_CODETYPE
+
+class UploadBundleRequest(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _UPLOADBUNDLEREQUEST
+
+class UploadBundleResponse(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _UPLOADBUNDLERESPONSE
+
+class UploadBundleContinue(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _UPLOADBUNDLECONTINUE
+
diff --git a/color.py b/color.py
new file mode 100644
index 0000000..b3a558c
--- /dev/null
+++ b/color.py
@@ -0,0 +1,154 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+import pager
+from git_config import GitConfig
+
+COLORS = {None     :-1,
+          'normal' :-1,
+          'black'  : 0,
+          'red'    : 1,
+          'green'  : 2,
+          'yellow' : 3,
+          'blue'   : 4,
+          'magenta': 5,
+          'cyan'   : 6,
+          'white'  : 7}
+
+ATTRS = {None     :-1,
+         'bold'   : 1,
+         'dim'    : 2,
+         'ul'     : 4,
+         'blink'  : 5,
+         'reverse': 7}
+
+RESET = "\033[m"
+
+def is_color(s): return s in COLORS
+def is_attr(s):  return s in ATTRS
+
+def _Color(fg = None, bg = None, attr = None):
+    fg = COLORS[fg]
+    bg = COLORS[bg]
+    attr = ATTRS[attr]
+
+    if attr >= 0 or fg >= 0 or bg >= 0:
+      need_sep = False
+      code = "\033["
+
+      if attr >= 0:
+        code += chr(ord('0') + attr)
+        need_sep = True
+
+      if fg >= 0:
+        if need_sep:
+          code += ';'
+        need_sep = True
+
+        if fg < 8:
+          code += '3%c' % (ord('0') + fg)
+        else:
+          code += '38;5;%d' % fg
+
+      if bg >= 0:
+        if need_sep:
+          code += ';'
+        need_sep = True
+
+        if bg < 8:
+          code += '4%c' % (ord('0') + bg)
+        else:
+          code += '48;5;%d' % bg
+      code += 'm'
+    else:
+      code = ''
+    return code
+
+
+class Coloring(object):
+  def __init__(self, config, type):
+    self._section = 'color.%s' % type
+    self._config = config
+    self._out = sys.stdout
+
+    on = self._config.GetString(self._section)
+    if on is None:
+      on = self._config.GetString('color.ui')
+
+    if on == 'auto':
+      if pager.active or os.isatty(1):
+        self._on = True
+      else:
+        self._on = False
+    elif on in ('true', 'always'):
+      self._on = True
+    else:
+      self._on = False
+
+  @property
+  def is_on(self):
+    return self._on
+
+  def write(self, fmt, *args):
+    self._out.write(fmt % args)
+
+  def nl(self):
+    self._out.write('\n')
+
+  def printer(self, opt=None, fg=None, bg=None, attr=None):
+    s = self
+    c = self.colorer(opt, fg, bg, attr)
+    def f(fmt, *args):
+      s._out.write(c(fmt, *args))
+    return f
+
+  def colorer(self, opt=None, fg=None, bg=None, attr=None):
+    if self._on:
+      c = self._parse(opt, fg, bg, attr)
+      def f(fmt, *args):
+        str = fmt % args
+        return ''.join([c, str, RESET])
+      return f
+    else:
+      def f(fmt, *args):
+        return fmt % args
+      return f
+
+  def _parse(self, opt, fg, bg, attr):
+    if not opt:
+      return _Color(fg, bg, attr)
+
+    v = self._config.GetString('%s.%s' % (self._section, opt))
+    if v is None:
+      return _Color(fg, bg, attr)
+
+    v = v.trim().lowercase()
+    if v == "reset":
+      return RESET
+    elif v == '':
+      return _Color(fg, bg, attr)
+
+    have_fg = False
+    for a in v.split(' '):
+      if is_color(a):
+        if have_fg: bg = a
+        else:       fg = a
+      elif is_attr(a):
+        attr = a
+
+    return _Color(fg, bg, attr)
diff --git a/command.py b/command.py
new file mode 100644
index 0000000..516c2d9
--- /dev/null
+++ b/command.py
@@ -0,0 +1,116 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import optparse
+import sys
+
+from error import NoSuchProjectError
+
+class Command(object):
+  """Base class for any command line action in repo.
+  """
+
+  common = False
+  manifest = None
+  _optparse = None
+
+  @property
+  def OptionParser(self):
+    if self._optparse is None:
+      try:
+        me = 'repo %s' % self.NAME
+        usage = self.helpUsage.strip().replace('%prog', me)
+      except AttributeError:
+        usage = 'repo %s' % self.NAME
+      self._optparse = optparse.OptionParser(usage = usage)
+      self._Options(self._optparse)
+    return self._optparse
+
+  def _Options(self, p):
+    """Initialize the option parser.
+    """
+
+  def Usage(self):
+    """Display usage and terminate.
+    """
+    self.OptionParser.print_usage()
+    sys.exit(1)
+
+  def Execute(self, opt, args):
+    """Perform the action, after option parsing is complete.
+    """
+    raise NotImplementedError
+ 
+  def GetProjects(self, args, missing_ok=False):
+    """A list of projects that match the arguments.
+    """
+    all = self.manifest.projects
+    result = []
+
+    if not args:
+      for project in all.values():
+        if missing_ok or project.Exists:
+          result.append(project)
+    else:
+      by_path = None
+
+      for arg in args:
+        project = all.get(arg)
+
+        if not project:
+          path = os.path.abspath(arg)
+
+          if not by_path:
+            by_path = dict()
+            for p in all.values():
+              by_path[p.worktree] = p
+
+          if os.path.exists(path):
+            while path \
+              and path != '/' \
+              and path != self.manifest.topdir:
+              try:
+                project = by_path[path]
+                break
+              except KeyError:
+                path = os.path.dirname(path)
+          else:
+            try:
+              project = by_path[path]
+            except KeyError:
+              pass
+
+        if not project:
+          raise NoSuchProjectError(arg)
+        if not missing_ok and not project.Exists:
+          raise NoSuchProjectError(arg)
+
+        result.append(project)
+
+    def _getpath(x):
+      return x.relpath
+    result.sort(key=_getpath)
+    return result
+
+class InteractiveCommand(Command):
+  """Command which requires user interaction on the tty and
+     must not run within a pager, even if the user asks to.
+  """
+
+class PagedCommand(Command):
+  """Command which defaults to output in a pager, as its
+     display tends to be larger than one screen full.
+  """
diff --git a/editor.py b/editor.py
new file mode 100644
index 0000000..4f22257
--- /dev/null
+++ b/editor.py
@@ -0,0 +1,85 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import subprocess
+import tempfile
+
+from error import EditorError
+
+class Editor(object):
+  """Manages the user's preferred text editor."""
+
+  _editor = None
+  globalConfig = None
+
+  @classmethod
+  def _GetEditor(cls):
+    if cls._editor is None:
+      cls._editor = cls._SelectEditor()
+    return cls._editor
+
+  @classmethod
+  def _SelectEditor(cls):
+    e = os.getenv('GIT_EDITOR')
+    if e:
+      return e
+
+    e = cls.globalConfig.GetString('core.editor')
+    if e:
+      return e
+
+    e = os.getenv('VISUAL')
+    if e:
+      return e
+
+    e = os.getenv('EDITOR')
+    if e:
+      return e
+
+    if os.getenv('TERM') == 'dumb':
+      print >>sys.stderr,\
+"""No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR.
+Tried to fall back to vi but terminal is dumb.  Please configure at
+least one of these before using this command."""
+      sys.exit(1)
+
+    return 'vi'
+
+  @classmethod
+  def EditString(cls, data):
+    """Opens an editor to edit the given content.
+
+       Args:
+         data        : the text to edit
+  
+      Returns:
+        new value of edited text; None if editing did not succeed
+    """
+    editor = cls._GetEditor()
+    fd, path = tempfile.mkstemp()
+    try:
+      os.write(fd, data)
+      os.close(fd)
+      fd = None
+
+      if subprocess.Popen([editor, path]).wait() != 0:
+        raise EditorError()
+      return open(path).read()
+    finally:
+      if fd:
+        os.close(fd)
+      os.remove(path)
diff --git a/error.py b/error.py
new file mode 100644
index 0000000..e3cf41c
--- /dev/null
+++ b/error.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class ManifestParseError(Exception):
+  """Failed to parse the manifest file.
+  """
+
+class EditorError(Exception):
+  """Unspecified error from the user's text editor.
+  """
+
+class GitError(Exception):
+  """Unspecified internal error from git.
+  """
+  def __init__(self, command):
+    self.command = command
+
+  def __str__(self):
+    return self.command
+
+class ImportError(Exception):
+  """An import from a non-Git format cannot be performed.
+  """
+  def __init__(self, reason):
+    self.reason = reason
+
+  def __str__(self):
+    return self.reason
+
+class UploadError(Exception):
+  """A bundle upload to Gerrit did not succeed.
+  """
+  def __init__(self, reason):
+    self.reason = reason
+
+  def __str__(self):
+    return self.reason
+
+class NoSuchProjectError(Exception):
+  """A specified project does not exist in the work tree.
+  """
+  def __init__(self, name=None):
+    self.name = name
+
+  def __str__(self):
+    if self.Name is None:
+      return 'in current directory'
+    return self.name
+
+class RepoChangedException(Exception):
+  """Thrown if 'repo sync' results in repo updating its internal
+     repo or manifest repositories.  In this special case we must
+     use exec to re-execute repo with the new code and manifest.
+  """
diff --git a/froofle/__init__.py b/froofle/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/froofle/__init__.py
diff --git a/froofle/protobuf/__init__.py b/froofle/protobuf/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/froofle/protobuf/__init__.py
diff --git a/froofle/protobuf/descriptor.py b/froofle/protobuf/descriptor.py
new file mode 100644
index 0000000..e74cf25
--- /dev/null
+++ b/froofle/protobuf/descriptor.py
@@ -0,0 +1,433 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+# TODO(robinson): We probably need to provide deep-copy methods for
+# descriptor types.  When a FieldDescriptor is passed into
+# Descriptor.__init__(), we should make a deep copy and then set
+# containing_type on it.  Alternatively, we could just get
+# rid of containing_type (iit's not needed for reflection.py, at least).
+#
+# TODO(robinson): Print method?
+#
+# TODO(robinson): Useful __repr__?
+
+"""Descriptors essentially contain exactly the information found in a .proto
+file, in types that make this information accessible in Python.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+class DescriptorBase(object):
+
+  """Descriptors base class.
+
+  This class is the base of all descriptor classes. It provides common options
+  related functionaility.
+  """
+
+  def __init__(self, options, options_class_name):
+    """Initialize the descriptor given its options message and the name of the
+    class of the options message. The name of the class is required in case
+    the options message is None and has to be created.
+    """
+    self._options = options
+    self._options_class_name = options_class_name
+
+  def GetOptions(self):
+    """Retrieves descriptor options.
+
+    This method returns the options set or creates the default options for the
+    descriptor.
+    """
+    if self._options:
+      return self._options
+    from froofle.protobuf import descriptor_pb2
+    try:
+      options_class = getattr(descriptor_pb2, self._options_class_name)
+    except AttributeError:
+      raise RuntimeError('Unknown options class name %s!' %
+                         (self._options_class_name))
+    self._options = options_class()
+    return self._options
+
+
+class Descriptor(DescriptorBase):
+
+  """Descriptor for a protocol message type.
+
+  A Descriptor instance has the following attributes:
+
+    name: (str) Name of this protocol message type.
+    full_name: (str) Fully-qualified name of this protocol message type,
+      which will include protocol "package" name and the name of any
+      enclosing types.
+
+    filename: (str) Name of the .proto file containing this message.
+
+    containing_type: (Descriptor) Reference to the descriptor of the
+      type containing us, or None if we have no containing type.
+
+    fields: (list of FieldDescriptors) Field descriptors for all
+      fields in this type.
+    fields_by_number: (dict int -> FieldDescriptor) Same FieldDescriptor
+      objects as in |fields|, but indexed by "number" attribute in each
+      FieldDescriptor.
+    fields_by_name: (dict str -> FieldDescriptor) Same FieldDescriptor
+      objects as in |fields|, but indexed by "name" attribute in each
+      FieldDescriptor.
+
+    nested_types: (list of Descriptors) Descriptor references
+      for all protocol message types nested within this one.
+    nested_types_by_name: (dict str -> Descriptor) Same Descriptor
+      objects as in |nested_types|, but indexed by "name" attribute
+      in each Descriptor.
+
+    enum_types: (list of EnumDescriptors) EnumDescriptor references
+      for all enums contained within this type.
+    enum_types_by_name: (dict str ->EnumDescriptor) Same EnumDescriptor
+      objects as in |enum_types|, but indexed by "name" attribute
+      in each EnumDescriptor.
+    enum_values_by_name: (dict str -> EnumValueDescriptor) Dict mapping
+      from enum value name to EnumValueDescriptor for that value.
+
+    extensions: (list of FieldDescriptor) All extensions defined directly
+      within this message type (NOT within a nested type).
+    extensions_by_name: (dict, string -> FieldDescriptor) Same FieldDescriptor
+      objects as |extensions|, but indexed by "name" attribute of each
+      FieldDescriptor.
+
+    options: (descriptor_pb2.MessageOptions) Protocol message options or None
+      to use default message options.
+  """
+
+  def __init__(self, name, full_name, filename, containing_type,
+               fields, nested_types, enum_types, extensions, options=None):
+    """Arguments to __init__() are as described in the description
+    of Descriptor fields above.
+    """
+    super(Descriptor, self).__init__(options, 'MessageOptions')
+    self.name = name
+    self.full_name = full_name
+    self.filename = filename
+    self.containing_type = containing_type
+
+    # We have fields in addition to fields_by_name and fields_by_number,
+    # so that:
+    #   1. Clients can index fields by "order in which they're listed."
+    #   2. Clients can easily iterate over all fields with the terse
+    #      syntax: for f in descriptor.fields: ...
+    self.fields = fields
+    for field in self.fields:
+      field.containing_type = self
+    self.fields_by_number = dict((f.number, f) for f in fields)
+    self.fields_by_name = dict((f.name, f) for f in fields)
+
+    self.nested_types = nested_types
+    self.nested_types_by_name = dict((t.name, t) for t in nested_types)
+
+    self.enum_types = enum_types
+    for enum_type in self.enum_types:
+      enum_type.containing_type = self
+    self.enum_types_by_name = dict((t.name, t) for t in enum_types)
+    self.enum_values_by_name = dict(
+        (v.name, v) for t in enum_types for v in t.values)
+
+    self.extensions = extensions
+    for extension in self.extensions:
+      extension.extension_scope = self
+    self.extensions_by_name = dict((f.name, f) for f in extensions)
+
+
+# TODO(robinson): We should have aggressive checking here,
+# for example:
+#   * If you specify a repeated field, you should not be allowed
+#     to specify a default value.
+#   * [Other examples here as needed].
+#
+# TODO(robinson): for this and other *Descriptor classes, we
+# might also want to lock things down aggressively (e.g.,
+# prevent clients from setting the attributes).  Having
+# stronger invariants here in general will reduce the number
+# of runtime checks we must do in reflection.py...
+class FieldDescriptor(DescriptorBase):
+
+  """Descriptor for a single field in a .proto file.
+
+  A FieldDescriptor instance has the following attriubtes:
+
+    name: (str) Name of this field, exactly as it appears in .proto.
+    full_name: (str) Name of this field, including containing scope.  This is
+      particularly relevant for extensions.
+    index: (int) Dense, 0-indexed index giving the order that this
+      field textually appears within its message in the .proto file.
+    number: (int) Tag number declared for this field in the .proto file.
+
+    type: (One of the TYPE_* constants below) Declared type.
+    cpp_type: (One of the CPPTYPE_* constants below) C++ type used to
+      represent this field.
+
+    label: (One of the LABEL_* constants below) Tells whether this
+      field is optional, required, or repeated.
+    default_value: (Varies) Default value of this field.  Only
+      meaningful for non-repeated scalar fields.  Repeated fields
+      should always set this to [], and non-repeated composite
+      fields should always set this to None.
+
+    containing_type: (Descriptor) Descriptor of the protocol message
+      type that contains this field.  Set by the Descriptor constructor
+      if we're passed into one.
+      Somewhat confusingly, for extension fields, this is the
+      descriptor of the EXTENDED message, not the descriptor
+      of the message containing this field.  (See is_extension and
+      extension_scope below).
+    message_type: (Descriptor) If a composite field, a descriptor
+      of the message type contained in this field.  Otherwise, this is None.
+    enum_type: (EnumDescriptor) If this field contains an enum, a
+      descriptor of that enum.  Otherwise, this is None.
+
+    is_extension: True iff this describes an extension field.
+    extension_scope: (Descriptor) Only meaningful if is_extension is True.
+      Gives the message that immediately contains this extension field.
+      Will be None iff we're a top-level (file-level) extension field.
+
+    options: (descriptor_pb2.FieldOptions) Protocol message field options or
+      None to use default field options.
+  """
+
+  # Must be consistent with C++ FieldDescriptor::Type enum in
+  # descriptor.h.
+  #
+  # TODO(robinson): Find a way to eliminate this repetition.
+  TYPE_DOUBLE         = 1
+  TYPE_FLOAT          = 2
+  TYPE_INT64          = 3
+  TYPE_UINT64         = 4
+  TYPE_INT32          = 5
+  TYPE_FIXED64        = 6
+  TYPE_FIXED32        = 7
+  TYPE_BOOL           = 8
+  TYPE_STRING         = 9
+  TYPE_GROUP          = 10
+  TYPE_MESSAGE        = 11
+  TYPE_BYTES          = 12
+  TYPE_UINT32         = 13
+  TYPE_ENUM           = 14
+  TYPE_SFIXED32       = 15
+  TYPE_SFIXED64       = 16
+  TYPE_SINT32         = 17
+  TYPE_SINT64         = 18
+  MAX_TYPE            = 18
+
+  # Must be consistent with C++ FieldDescriptor::CppType enum in
+  # descriptor.h.
+  #
+  # TODO(robinson): Find a way to eliminate this repetition.
+  CPPTYPE_INT32       = 1
+  CPPTYPE_INT64       = 2
+  CPPTYPE_UINT32      = 3
+  CPPTYPE_UINT64      = 4
+  CPPTYPE_DOUBLE      = 5
+  CPPTYPE_FLOAT       = 6
+  CPPTYPE_BOOL        = 7
+  CPPTYPE_ENUM        = 8
+  CPPTYPE_STRING      = 9
+  CPPTYPE_MESSAGE     = 10
+  MAX_CPPTYPE         = 10
+
+  # Must be consistent with C++ FieldDescriptor::Label enum in
+  # descriptor.h.
+  #
+  # TODO(robinson): Find a way to eliminate this repetition.
+  LABEL_OPTIONAL      = 1
+  LABEL_REQUIRED      = 2
+  LABEL_REPEATED      = 3
+  MAX_LABEL           = 3
+
+  def __init__(self, name, full_name, index, number, type, cpp_type, label,
+               default_value, message_type, enum_type, containing_type,
+               is_extension, extension_scope, options=None):
+    """The arguments are as described in the description of FieldDescriptor
+    attributes above.
+
+    Note that containing_type may be None, and may be set later if necessary
+    (to deal with circular references between message types, for example).
+    Likewise for extension_scope.
+    """
+    super(FieldDescriptor, self).__init__(options, 'FieldOptions')
+    self.name = name
+    self.full_name = full_name
+    self.index = index
+    self.number = number
+    self.type = type
+    self.cpp_type = cpp_type
+    self.label = label
+    self.default_value = default_value
+    self.containing_type = containing_type
+    self.message_type = message_type
+    self.enum_type = enum_type
+    self.is_extension = is_extension
+    self.extension_scope = extension_scope
+
+
+class EnumDescriptor(DescriptorBase):
+
+  """Descriptor for an enum defined in a .proto file.
+
+  An EnumDescriptor instance has the following attributes:
+
+    name: (str) Name of the enum type.
+    full_name: (str) Full name of the type, including package name
+      and any enclosing type(s).
+    filename: (str) Name of the .proto file in which this appears.
+
+    values: (list of EnumValueDescriptors) List of the values
+      in this enum.
+    values_by_name: (dict str -> EnumValueDescriptor) Same as |values|,
+      but indexed by the "name" field of each EnumValueDescriptor.
+    values_by_number: (dict int -> EnumValueDescriptor) Same as |values|,
+      but indexed by the "number" field of each EnumValueDescriptor.
+    containing_type: (Descriptor) Descriptor of the immediate containing
+      type of this enum, or None if this is an enum defined at the
+      top level in a .proto file.  Set by Descriptor's constructor
+      if we're passed into one.
+    options: (descriptor_pb2.EnumOptions) Enum options message or
+      None to use default enum options.
+  """
+
+  def __init__(self, name, full_name, filename, values,
+               containing_type=None, options=None):
+    """Arguments are as described in the attribute description above."""
+    super(EnumDescriptor, self).__init__(options, 'EnumOptions')
+    self.name = name
+    self.full_name = full_name
+    self.filename = filename
+    self.values = values
+    for value in self.values:
+      value.type = self
+    self.values_by_name = dict((v.name, v) for v in values)
+    self.values_by_number = dict((v.number, v) for v in values)
+    self.containing_type = containing_type
+
+
+class EnumValueDescriptor(DescriptorBase):
+
+  """Descriptor for a single value within an enum.
+
+    name: (str) Name of this value.
+    index: (int) Dense, 0-indexed index giving the order that this
+      value appears textually within its enum in the .proto file.
+    number: (int) Actual number assigned to this enum value.
+    type: (EnumDescriptor) EnumDescriptor to which this value
+      belongs.  Set by EnumDescriptor's constructor if we're
+      passed into one.
+    options: (descriptor_pb2.EnumValueOptions) Enum value options message or
+      None to use default enum value options options.
+  """
+
+  def __init__(self, name, index, number, type=None, options=None):
+    """Arguments are as described in the attribute description above."""
+    super(EnumValueDescriptor, self).__init__(options, 'EnumValueOptions')
+    self.name = name
+    self.index = index
+    self.number = number
+    self.type = type
+
+
+class ServiceDescriptor(DescriptorBase):
+
+  """Descriptor for a service.
+
+    name: (str) Name of the service.
+    full_name: (str) Full name of the service, including package name.
+    index: (int) 0-indexed index giving the order that this services
+      definition appears withing the .proto file.
+    methods: (list of MethodDescriptor) List of methods provided by this
+      service.
+    options: (descriptor_pb2.ServiceOptions) Service options message or
+      None to use default service options.
+  """
+
+  def __init__(self, name, full_name, index, methods, options=None):
+    super(ServiceDescriptor, self).__init__(options, 'ServiceOptions')
+    self.name = name
+    self.full_name = full_name
+    self.index = index
+    self.methods = methods
+    # Set the containing service for each method in this service.
+    for method in self.methods:
+      method.containing_service = self
+
+  def FindMethodByName(self, name):
+    """Searches for the specified method, and returns its descriptor."""
+    for method in self.methods:
+      if name == method.name:
+        return method
+    return None
+
+
+class MethodDescriptor(DescriptorBase):
+
+  """Descriptor for a method in a service.
+
+  name: (str) Name of the method within the service.
+  full_name: (str) Full name of method.
+  index: (int) 0-indexed index of the method inside the service.
+  containing_service: (ServiceDescriptor) The service that contains this
+    method.
+  input_type: The descriptor of the message that this method accepts.
+  output_type: The descriptor of the message that this method returns.
+  options: (descriptor_pb2.MethodOptions) Method options message or
+    None to use default method options.
+  """
+
+  def __init__(self, name, full_name, index, containing_service,
+               input_type, output_type, options=None):
+    """The arguments are as described in the description of MethodDescriptor
+    attributes above.
+
+    Note that containing_service may be None, and may be set later if necessary.
+    """
+    super(MethodDescriptor, self).__init__(options, 'MethodOptions')
+    self.name = name
+    self.full_name = full_name
+    self.index = index
+    self.containing_service = containing_service
+    self.input_type = input_type
+    self.output_type = output_type
+
+
+def _ParseOptions(message, string):
+  """Parses serialized options.
+
+  This helper function is used to parse serialized options in generated
+  proto2 files. It must not be used outside proto2.
+  """
+  message.ParseFromString(string)
+  return message;
diff --git a/froofle/protobuf/descriptor_pb2.py b/froofle/protobuf/descriptor_pb2.py
new file mode 100644
index 0000000..1687383
--- /dev/null
+++ b/froofle/protobuf/descriptor_pb2.py
@@ -0,0 +1,950 @@
+#!/usr/bin/python2.4
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+
+from froofle.protobuf import descriptor
+from froofle.protobuf import message
+from froofle.protobuf import reflection
+from froofle.protobuf import service
+from froofle.protobuf import service_reflection
+
+
+_FIELDDESCRIPTORPROTO_TYPE = descriptor.EnumDescriptor(
+  name='Type',
+  full_name='froofle.protobuf.FieldDescriptorProto.Type',
+  filename='Type',
+  values=[
+    descriptor.EnumValueDescriptor(
+      name='TYPE_DOUBLE', index=0, number=1,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_FLOAT', index=1, number=2,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_INT64', index=2, number=3,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_UINT64', index=3, number=4,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_INT32', index=4, number=5,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_FIXED64', index=5, number=6,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_FIXED32', index=6, number=7,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_BOOL', index=7, number=8,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_STRING', index=8, number=9,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_GROUP', index=9, number=10,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_MESSAGE', index=10, number=11,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_BYTES', index=11, number=12,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_UINT32', index=12, number=13,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_ENUM', index=13, number=14,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_SFIXED32', index=14, number=15,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_SFIXED64', index=15, number=16,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_SINT32', index=16, number=17,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='TYPE_SINT64', index=17, number=18,
+      options=None,
+      type=None),
+  ],
+  options=None,
+)
+
+_FIELDDESCRIPTORPROTO_LABEL = descriptor.EnumDescriptor(
+  name='Label',
+  full_name='froofle.protobuf.FieldDescriptorProto.Label',
+  filename='Label',
+  values=[
+    descriptor.EnumValueDescriptor(
+      name='LABEL_OPTIONAL', index=0, number=1,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='LABEL_REQUIRED', index=1, number=2,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='LABEL_REPEATED', index=2, number=3,
+      options=None,
+      type=None),
+  ],
+  options=None,
+)
+
+_FILEOPTIONS_OPTIMIZEMODE = descriptor.EnumDescriptor(
+  name='OptimizeMode',
+  full_name='froofle.protobuf.FileOptions.OptimizeMode',
+  filename='OptimizeMode',
+  values=[
+    descriptor.EnumValueDescriptor(
+      name='SPEED', index=0, number=1,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='CODE_SIZE', index=1, number=2,
+      options=None,
+      type=None),
+  ],
+  options=None,
+)
+
+_FIELDOPTIONS_CTYPE = descriptor.EnumDescriptor(
+  name='CType',
+  full_name='froofle.protobuf.FieldOptions.CType',
+  filename='CType',
+  values=[
+    descriptor.EnumValueDescriptor(
+      name='CORD', index=0, number=1,
+      options=None,
+      type=None),
+    descriptor.EnumValueDescriptor(
+      name='STRING_PIECE', index=1, number=2,
+      options=None,
+      type=None),
+  ],
+  options=None,
+)
+
+
+_FILEDESCRIPTORSET = descriptor.Descriptor(
+  name='FileDescriptorSet',
+  full_name='froofle.protobuf.FileDescriptorSet',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='file', full_name='froofle.protobuf.FileDescriptorSet.file', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_FILEDESCRIPTORPROTO = descriptor.Descriptor(
+  name='FileDescriptorProto',
+  full_name='froofle.protobuf.FileDescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.FileDescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='package', full_name='froofle.protobuf.FileDescriptorProto.package', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='dependency', full_name='froofle.protobuf.FileDescriptorProto.dependency', index=2,
+      number=3, type=9, cpp_type=9, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='message_type', full_name='froofle.protobuf.FileDescriptorProto.message_type', index=3,
+      number=4, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='enum_type', full_name='froofle.protobuf.FileDescriptorProto.enum_type', index=4,
+      number=5, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='service', full_name='froofle.protobuf.FileDescriptorProto.service', index=5,
+      number=6, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='extension', full_name='froofle.protobuf.FileDescriptorProto.extension', index=6,
+      number=7, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.FileDescriptorProto.options', index=7,
+      number=8, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_DESCRIPTORPROTO_EXTENSIONRANGE = descriptor.Descriptor(
+  name='ExtensionRange',
+  full_name='froofle.protobuf.DescriptorProto.ExtensionRange',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='start', full_name='froofle.protobuf.DescriptorProto.ExtensionRange.start', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='end', full_name='froofle.protobuf.DescriptorProto.ExtensionRange.end', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+_DESCRIPTORPROTO = descriptor.Descriptor(
+  name='DescriptorProto',
+  full_name='froofle.protobuf.DescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.DescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='field', full_name='froofle.protobuf.DescriptorProto.field', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='extension', full_name='froofle.protobuf.DescriptorProto.extension', index=2,
+      number=6, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='nested_type', full_name='froofle.protobuf.DescriptorProto.nested_type', index=3,
+      number=3, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='enum_type', full_name='froofle.protobuf.DescriptorProto.enum_type', index=4,
+      number=4, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='extension_range', full_name='froofle.protobuf.DescriptorProto.extension_range', index=5,
+      number=5, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.DescriptorProto.options', index=6,
+      number=7, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_FIELDDESCRIPTORPROTO = descriptor.Descriptor(
+  name='FieldDescriptorProto',
+  full_name='froofle.protobuf.FieldDescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.FieldDescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='number', full_name='froofle.protobuf.FieldDescriptorProto.number', index=1,
+      number=3, type=5, cpp_type=1, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='label', full_name='froofle.protobuf.FieldDescriptorProto.label', index=2,
+      number=4, type=14, cpp_type=8, label=1,
+      default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='type', full_name='froofle.protobuf.FieldDescriptorProto.type', index=3,
+      number=5, type=14, cpp_type=8, label=1,
+      default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='type_name', full_name='froofle.protobuf.FieldDescriptorProto.type_name', index=4,
+      number=6, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='extendee', full_name='froofle.protobuf.FieldDescriptorProto.extendee', index=5,
+      number=2, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='default_value', full_name='froofle.protobuf.FieldDescriptorProto.default_value', index=6,
+      number=7, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.FieldDescriptorProto.options', index=7,
+      number=8, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+    _FIELDDESCRIPTORPROTO_TYPE,
+    _FIELDDESCRIPTORPROTO_LABEL,
+  ],
+  options=None)
+
+
+_ENUMDESCRIPTORPROTO = descriptor.Descriptor(
+  name='EnumDescriptorProto',
+  full_name='froofle.protobuf.EnumDescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.EnumDescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='value', full_name='froofle.protobuf.EnumDescriptorProto.value', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.EnumDescriptorProto.options', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_ENUMVALUEDESCRIPTORPROTO = descriptor.Descriptor(
+  name='EnumValueDescriptorProto',
+  full_name='froofle.protobuf.EnumValueDescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.EnumValueDescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='number', full_name='froofle.protobuf.EnumValueDescriptorProto.number', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.EnumValueDescriptorProto.options', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_SERVICEDESCRIPTORPROTO = descriptor.Descriptor(
+  name='ServiceDescriptorProto',
+  full_name='froofle.protobuf.ServiceDescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.ServiceDescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='method', full_name='froofle.protobuf.ServiceDescriptorProto.method', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.ServiceDescriptorProto.options', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_METHODDESCRIPTORPROTO = descriptor.Descriptor(
+  name='MethodDescriptorProto',
+  full_name='froofle.protobuf.MethodDescriptorProto',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.MethodDescriptorProto.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='input_type', full_name='froofle.protobuf.MethodDescriptorProto.input_type', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='output_type', full_name='froofle.protobuf.MethodDescriptorProto.output_type', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='options', full_name='froofle.protobuf.MethodDescriptorProto.options', index=3,
+      number=4, type=11, cpp_type=10, label=1,
+      default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_FILEOPTIONS = descriptor.Descriptor(
+  name='FileOptions',
+  full_name='froofle.protobuf.FileOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='java_package', full_name='froofle.protobuf.FileOptions.java_package', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='java_outer_classname', full_name='froofle.protobuf.FileOptions.java_outer_classname', index=1,
+      number=8, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='java_multiple_files', full_name='froofle.protobuf.FileOptions.java_multiple_files', index=2,
+      number=10, type=8, cpp_type=7, label=1,
+      default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='optimize_for', full_name='froofle.protobuf.FileOptions.optimize_for', index=3,
+      number=9, type=14, cpp_type=8, label=1,
+      default_value=2,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.FileOptions.uninterpreted_option', index=4,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+    _FILEOPTIONS_OPTIMIZEMODE,
+  ],
+  options=None)
+
+
+_MESSAGEOPTIONS = descriptor.Descriptor(
+  name='MessageOptions',
+  full_name='froofle.protobuf.MessageOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='message_set_wire_format', full_name='froofle.protobuf.MessageOptions.message_set_wire_format', index=0,
+      number=1, type=8, cpp_type=7, label=1,
+      default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.MessageOptions.uninterpreted_option', index=1,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_FIELDOPTIONS = descriptor.Descriptor(
+  name='FieldOptions',
+  full_name='froofle.protobuf.FieldOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='ctype', full_name='froofle.protobuf.FieldOptions.ctype', index=0,
+      number=1, type=14, cpp_type=8, label=1,
+      default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='experimental_map_key', full_name='froofle.protobuf.FieldOptions.experimental_map_key', index=1,
+      number=9, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.FieldOptions.uninterpreted_option', index=2,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+    _FIELDOPTIONS_CTYPE,
+  ],
+  options=None)
+
+
+_ENUMOPTIONS = descriptor.Descriptor(
+  name='EnumOptions',
+  full_name='froofle.protobuf.EnumOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.EnumOptions.uninterpreted_option', index=0,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_ENUMVALUEOPTIONS = descriptor.Descriptor(
+  name='EnumValueOptions',
+  full_name='froofle.protobuf.EnumValueOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.EnumValueOptions.uninterpreted_option', index=0,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_SERVICEOPTIONS = descriptor.Descriptor(
+  name='ServiceOptions',
+  full_name='froofle.protobuf.ServiceOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.ServiceOptions.uninterpreted_option', index=0,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_METHODOPTIONS = descriptor.Descriptor(
+  name='MethodOptions',
+  full_name='froofle.protobuf.MethodOptions',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='uninterpreted_option', full_name='froofle.protobuf.MethodOptions.uninterpreted_option', index=0,
+      number=999, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_UNINTERPRETEDOPTION_NAMEPART = descriptor.Descriptor(
+  name='NamePart',
+  full_name='froofle.protobuf.UninterpretedOption.NamePart',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name_part', full_name='froofle.protobuf.UninterpretedOption.NamePart.name_part', index=0,
+      number=1, type=9, cpp_type=9, label=2,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='is_extension', full_name='froofle.protobuf.UninterpretedOption.NamePart.is_extension', index=1,
+      number=2, type=8, cpp_type=7, label=2,
+      default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+_UNINTERPRETEDOPTION = descriptor.Descriptor(
+  name='UninterpretedOption',
+  full_name='froofle.protobuf.UninterpretedOption',
+  filename='froofle/protobuf/descriptor.proto',
+  containing_type=None,
+  fields=[
+    descriptor.FieldDescriptor(
+      name='name', full_name='froofle.protobuf.UninterpretedOption.name', index=0,
+      number=2, type=11, cpp_type=10, label=3,
+      default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='identifier_value', full_name='froofle.protobuf.UninterpretedOption.identifier_value', index=1,
+      number=3, type=9, cpp_type=9, label=1,
+      default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='positive_int_value', full_name='froofle.protobuf.UninterpretedOption.positive_int_value', index=2,
+      number=4, type=4, cpp_type=4, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='negative_int_value', full_name='froofle.protobuf.UninterpretedOption.negative_int_value', index=3,
+      number=5, type=3, cpp_type=2, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='double_value', full_name='froofle.protobuf.UninterpretedOption.double_value', index=4,
+      number=6, type=1, cpp_type=5, label=1,
+      default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    descriptor.FieldDescriptor(
+      name='string_value', full_name='froofle.protobuf.UninterpretedOption.string_value', index=5,
+      number=7, type=12, cpp_type=9, label=1,
+      default_value="",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],  # TODO(robinson): Implement.
+  enum_types=[
+  ],
+  options=None)
+
+
+_FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO
+_FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO
+_FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO
+_FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO
+_FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO
+_FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS
+_DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO
+_DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO
+_DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO
+_DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO
+_DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE
+_DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS
+_FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL
+_FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE
+_FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS
+_ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO
+_ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS
+_ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS
+_SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO
+_SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS
+_METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS
+_FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE
+_FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE
+_FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION
+_UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART
+
+class FileDescriptorSet(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _FILEDESCRIPTORSET
+
+class FileDescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _FILEDESCRIPTORPROTO
+
+class DescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  
+  class ExtensionRange(message.Message):
+    __metaclass__ = reflection.GeneratedProtocolMessageType
+    DESCRIPTOR = _DESCRIPTORPROTO_EXTENSIONRANGE
+  DESCRIPTOR = _DESCRIPTORPROTO
+
+class FieldDescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _FIELDDESCRIPTORPROTO
+
+class EnumDescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _ENUMDESCRIPTORPROTO
+
+class EnumValueDescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _ENUMVALUEDESCRIPTORPROTO
+
+class ServiceDescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _SERVICEDESCRIPTORPROTO
+
+class MethodDescriptorProto(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _METHODDESCRIPTORPROTO
+
+class FileOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _FILEOPTIONS
+
+class MessageOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _MESSAGEOPTIONS
+
+class FieldOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _FIELDOPTIONS
+
+class EnumOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _ENUMOPTIONS
+
+class EnumValueOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _ENUMVALUEOPTIONS
+
+class ServiceOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _SERVICEOPTIONS
+
+class MethodOptions(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  DESCRIPTOR = _METHODOPTIONS
+
+class UninterpretedOption(message.Message):
+  __metaclass__ = reflection.GeneratedProtocolMessageType
+  
+  class NamePart(message.Message):
+    __metaclass__ = reflection.GeneratedProtocolMessageType
+    DESCRIPTOR = _UNINTERPRETEDOPTION_NAMEPART
+  DESCRIPTOR = _UNINTERPRETEDOPTION
+
diff --git a/froofle/protobuf/internal/__init__.py b/froofle/protobuf/internal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/froofle/protobuf/internal/__init__.py
diff --git a/froofle/protobuf/internal/decoder.py b/froofle/protobuf/internal/decoder.py
new file mode 100644
index 0000000..2dd4c96
--- /dev/null
+++ b/froofle/protobuf/internal/decoder.py
@@ -0,0 +1,209 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Class for decoding protocol buffer primitives.
+
+Contains the logic for decoding every logical protocol field type
+from one of the 5 physical wire types.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+import struct
+from froofle.protobuf import message
+from froofle.protobuf.internal import input_stream
+from froofle.protobuf.internal import wire_format
+
+
+
+# Note that much of this code is ported from //net/proto/ProtocolBuffer, and
+# that the interface is strongly inspired by WireFormat from the C++ proto2
+# implementation.
+
+
+class Decoder(object):
+
+  """Decodes logical protocol buffer fields from the wire."""
+
+  def __init__(self, s):
+    """Initializes the decoder to read from s.
+
+    Args:
+      s: An immutable sequence of bytes, which must be accessible
+        via the Python buffer() primitive (i.e., buffer(s)).
+    """
+    self._stream = input_stream.InputStream(s)
+
+  def EndOfStream(self):
+    """Returns true iff we've reached the end of the bytes we're reading."""
+    return self._stream.EndOfStream()
+
+  def Position(self):
+    """Returns the 0-indexed position in |s|."""
+    return self._stream.Position()
+
+  def ReadFieldNumberAndWireType(self):
+    """Reads a tag from the wire. Returns a (field_number, wire_type) pair."""
+    tag_and_type = self.ReadUInt32()
+    return wire_format.UnpackTag(tag_and_type)
+
+  def SkipBytes(self, bytes):
+    """Skips the specified number of bytes on the wire."""
+    self._stream.SkipBytes(bytes)
+
+  # Note that the Read*() methods below are not exactly symmetrical with the
+  # corresponding Encoder.Append*() methods.  Those Encoder methods first
+  # encode a tag, but the Read*() methods below assume that the tag has already
+  # been read, and that the client wishes to read a field of the specified type
+  # starting at the current position.
+
+  def ReadInt32(self):
+    """Reads and returns a signed, varint-encoded, 32-bit integer."""
+    return self._stream.ReadVarint32()
+
+  def ReadInt64(self):
+    """Reads and returns a signed, varint-encoded, 64-bit integer."""
+    return self._stream.ReadVarint64()
+
+  def ReadUInt32(self):
+    """Reads and returns an signed, varint-encoded, 32-bit integer."""
+    return self._stream.ReadVarUInt32()
+
+  def ReadUInt64(self):
+    """Reads and returns an signed, varint-encoded,64-bit integer."""
+    return self._stream.ReadVarUInt64()
+
+  def ReadSInt32(self):
+    """Reads and returns a signed, zigzag-encoded, varint-encoded,
+    32-bit integer."""
+    return wire_format.ZigZagDecode(self._stream.ReadVarUInt32())
+
+  def ReadSInt64(self):
+    """Reads and returns a signed, zigzag-encoded, varint-encoded,
+    64-bit integer."""
+    return wire_format.ZigZagDecode(self._stream.ReadVarUInt64())
+
+  def ReadFixed32(self):
+    """Reads and returns an unsigned, fixed-width, 32-bit integer."""
+    return self._stream.ReadLittleEndian32()
+
+  def ReadFixed64(self):
+    """Reads and returns an unsigned, fixed-width, 64-bit integer."""
+    return self._stream.ReadLittleEndian64()
+
+  def ReadSFixed32(self):
+    """Reads and returns a signed, fixed-width, 32-bit integer."""
+    value = self._stream.ReadLittleEndian32()
+    if value >= (1 << 31):
+      value -= (1 << 32)
+    return value
+
+  def ReadSFixed64(self):
+    """Reads and returns a signed, fixed-width, 64-bit integer."""
+    value = self._stream.ReadLittleEndian64()
+    if value >= (1 << 63):
+      value -= (1 << 64)
+    return value
+
+  def ReadFloat(self):
+    """Reads and returns a 4-byte floating-point number."""
+    serialized = self._stream.ReadBytes(4)
+    return struct.unpack('f', serialized)[0]
+
+  def ReadDouble(self):
+    """Reads and returns an 8-byte floating-point number."""
+    serialized = self._stream.ReadBytes(8)
+    return struct.unpack('d', serialized)[0]
+
+  def ReadBool(self):
+    """Reads and returns a bool."""
+    i = self._stream.ReadVarUInt32()
+    return bool(i)
+
+  def ReadEnum(self):
+    """Reads and returns an enum value."""
+    return self._stream.ReadVarUInt32()
+
+  def ReadString(self):
+    """Reads and returns a length-delimited string."""
+    bytes = self.ReadBytes()
+    return unicode(bytes, 'utf-8')
+
+  def ReadBytes(self):
+    """Reads and returns a length-delimited byte sequence."""
+    length = self._stream.ReadVarUInt32()
+    return self._stream.ReadBytes(length)
+
+  def ReadMessageInto(self, msg):
+    """Calls msg.MergeFromString() to merge
+    length-delimited serialized message data into |msg|.
+
+    REQUIRES: The decoder must be positioned at the serialized "length"
+      prefix to a length-delmiited serialized message.
+
+    POSTCONDITION: The decoder is positioned just after the
+      serialized message, and we have merged those serialized
+      contents into |msg|.
+    """
+    length = self._stream.ReadVarUInt32()
+    sub_buffer = self._stream.GetSubBuffer(length)
+    num_bytes_used = msg.MergeFromString(sub_buffer)
+    if num_bytes_used != length:
+      raise message.DecodeError(
+          'Submessage told to deserialize from %d-byte encoding, '
+          'but used only %d bytes' % (length, num_bytes_used))
+    self._stream.SkipBytes(num_bytes_used)
+
+  def ReadGroupInto(self, expected_field_number, group):
+    """Calls group.MergeFromString() to merge
+    END_GROUP-delimited serialized message data into |group|.
+    We'll raise an exception if we don't find an END_GROUP
+    tag immediately after the serialized message contents.
+
+    REQUIRES: The decoder is positioned just after the START_GROUP
+      tag for this group.
+
+    POSTCONDITION: The decoder is positioned just after the
+      END_GROUP tag for this group, and we have merged
+      the contents of the group into |group|.
+    """
+    sub_buffer = self._stream.GetSubBuffer()  # No a priori length limit.
+    num_bytes_used = group.MergeFromString(sub_buffer)
+    if num_bytes_used < 0:
+      raise message.DecodeError('Group message reported negative bytes read.')
+    self._stream.SkipBytes(num_bytes_used)
+    field_number, field_type = self.ReadFieldNumberAndWireType()
+    if field_type != wire_format.WIRETYPE_END_GROUP:
+      raise message.DecodeError('Group message did not end with an END_GROUP.')
+    if field_number != expected_field_number:
+      raise message.DecodeError('END_GROUP tag had field '
+                                'number %d, was expecting field number %d' % (
+          field_number, expected_field_number))
+    # We're now positioned just after the END_GROUP tag.  Perfect.
diff --git a/froofle/protobuf/internal/encoder.py b/froofle/protobuf/internal/encoder.py
new file mode 100644
index 0000000..8b924b3
--- /dev/null
+++ b/froofle/protobuf/internal/encoder.py
@@ -0,0 +1,206 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Class for encoding protocol message primitives.
+
+Contains the logic for encoding every logical protocol field type
+into one of the 5 physical wire types.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+import struct
+from froofle.protobuf import message
+from froofle.protobuf.internal import wire_format
+from froofle.protobuf.internal import output_stream
+
+
+# Note that much of this code is ported from //net/proto/ProtocolBuffer, and
+# that the interface is strongly inspired by WireFormat from the C++ proto2
+# implementation.
+
+
+class Encoder(object):
+
+  """Encodes logical protocol buffer fields to the wire format."""
+
+  def __init__(self):
+    self._stream = output_stream.OutputStream()
+
+  def ToString(self):
+    """Returns all values encoded in this object as a string."""
+    return self._stream.ToString()
+
+  # All the Append*() methods below first append a tag+type pair to the buffer
+  # before appending the specified value.
+
+  def AppendInt32(self, field_number, value):
+    """Appends a 32-bit integer to our buffer, varint-encoded."""
+    self._AppendTag(field_number, wire_format.WIRETYPE_VARINT)
+    self._stream.AppendVarint32(value)
+
+  def AppendInt64(self, field_number, value):
+    """Appends a 64-bit integer to our buffer, varint-encoded."""
+    self._AppendTag(field_number, wire_format.WIRETYPE_VARINT)
+    self._stream.AppendVarint64(value)
+
+  def AppendUInt32(self, field_number, unsigned_value):
+    """Appends an unsigned 32-bit integer to our buffer, varint-encoded."""
+    self._AppendTag(field_number, wire_format.WIRETYPE_VARINT)
+    self._stream.AppendVarUInt32(unsigned_value)
+
+  def AppendUInt64(self, field_number, unsigned_value):
+    """Appends an unsigned 64-bit integer to our buffer, varint-encoded."""
+    self._AppendTag(field_number, wire_format.WIRETYPE_VARINT)
+    self._stream.AppendVarUInt64(unsigned_value)
+
+  def AppendSInt32(self, field_number, value):
+    """Appends a 32-bit integer to our buffer, zigzag-encoded and then
+    varint-encoded.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_VARINT)
+    zigzag_value = wire_format.ZigZagEncode(value)
+    self._stream.AppendVarUInt32(zigzag_value)
+
+  def AppendSInt64(self, field_number, value):
+    """Appends a 64-bit integer to our buffer, zigzag-encoded and then
+    varint-encoded.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_VARINT)
+    zigzag_value = wire_format.ZigZagEncode(value)
+    self._stream.AppendVarUInt64(zigzag_value)
+
+  def AppendFixed32(self, field_number, unsigned_value):
+    """Appends an unsigned 32-bit integer to our buffer, in little-endian
+    byte-order.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_FIXED32)
+    self._stream.AppendLittleEndian32(unsigned_value)
+
+  def AppendFixed64(self, field_number, unsigned_value):
+    """Appends an unsigned 64-bit integer to our buffer, in little-endian
+    byte-order.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_FIXED64)
+    self._stream.AppendLittleEndian64(unsigned_value)
+
+  def AppendSFixed32(self, field_number, value):
+    """Appends a signed 32-bit integer to our buffer, in little-endian
+    byte-order.
+    """
+    sign = (value & 0x80000000) and -1 or 0
+    if value >> 32 != sign:
+      raise message.EncodeError('SFixed32 out of range: %d' % value)
+    self._AppendTag(field_number, wire_format.WIRETYPE_FIXED32)
+    self._stream.AppendLittleEndian32(value & 0xffffffff)
+
+  def AppendSFixed64(self, field_number, value):
+    """Appends a signed 64-bit integer to our buffer, in little-endian
+    byte-order.
+    """
+    sign = (value & 0x8000000000000000) and -1 or 0
+    if value >> 64 != sign:
+      raise message.EncodeError('SFixed64 out of range: %d' % value)
+    self._AppendTag(field_number, wire_format.WIRETYPE_FIXED64)
+    self._stream.AppendLittleEndian64(value & 0xffffffffffffffff)
+
+  def AppendFloat(self, field_number, value):
+    """Appends a floating-point number to our buffer."""
+    self._AppendTag(field_number, wire_format.WIRETYPE_FIXED32)
+    self._stream.AppendRawBytes(struct.pack('f', value))
+
+  def AppendDouble(self, field_number, value):
+    """Appends a double-precision floating-point number to our buffer."""
+    self._AppendTag(field_number, wire_format.WIRETYPE_FIXED64)
+    self._stream.AppendRawBytes(struct.pack('d', value))
+
+  def AppendBool(self, field_number, value):
+    """Appends a boolean to our buffer."""
+    self.AppendInt32(field_number, value)
+
+  def AppendEnum(self, field_number, value):
+    """Appends an enum value to our buffer."""
+    self.AppendInt32(field_number, value)
+
+  def AppendString(self, field_number, value):
+    """Appends a length-prefixed unicode string, encoded as UTF-8 to our buffer,
+    with the length varint-encoded.
+    """
+    self.AppendBytes(field_number, value.encode('utf-8'))
+
+  def AppendBytes(self, field_number, value):
+    """Appends a length-prefixed sequence of bytes to our buffer, with the
+    length varint-encoded.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
+    self._stream.AppendVarUInt32(len(value))
+    self._stream.AppendRawBytes(value)
+
+  # TODO(robinson): For AppendGroup() and AppendMessage(), we'd really like to
+  # avoid the extra string copy here.  We can do so if we widen the Message
+  # interface to be able to serialize to a stream in addition to a string.  The
+  # challenge when thinking ahead to the Python/C API implementation of Message
+  # is finding a stream-like Python thing to which we can write raw bytes
+  # from C.  I'm not sure such a thing exists(?).  (array.array is pretty much
+  # what we want, but it's not directly exposed in the Python/C API).
+
+  def AppendGroup(self, field_number, group):
+    """Appends a group to our buffer.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_START_GROUP)
+    self._stream.AppendRawBytes(group.SerializeToString())
+    self._AppendTag(field_number, wire_format.WIRETYPE_END_GROUP)
+
+  def AppendMessage(self, field_number, msg):
+    """Appends a nested message to our buffer.
+    """
+    self._AppendTag(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED)
+    self._stream.AppendVarUInt32(msg.ByteSize())
+    self._stream.AppendRawBytes(msg.SerializeToString())
+
+  def AppendMessageSetItem(self, field_number, msg):
+    """Appends an item using the message set wire format.
+
+    The message set message looks like this:
+      message MessageSet {
+        repeated group Item = 1 {
+          required int32 type_id = 2;
+          required string message = 3;
+        }
+      }
+    """
+    self._AppendTag(1, wire_format.WIRETYPE_START_GROUP)
+    self.AppendInt32(2, field_number)
+    self.AppendMessage(3, msg)
+    self._AppendTag(1, wire_format.WIRETYPE_END_GROUP)
+
+  def _AppendTag(self, field_number, wire_type):
+    """Appends a tag containing field number and wire type information."""
+    self._stream.AppendVarUInt32(wire_format.PackTag(field_number, wire_type))
diff --git a/froofle/protobuf/internal/input_stream.py b/froofle/protobuf/internal/input_stream.py
new file mode 100644
index 0000000..26a26dc
--- /dev/null
+++ b/froofle/protobuf/internal/input_stream.py
@@ -0,0 +1,326 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""InputStream is the primitive interface for reading bits from the wire.
+
+All protocol buffer deserialization can be expressed in terms of
+the InputStream primitives provided here.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+import struct
+from array import array
+from froofle.protobuf import message
+from froofle.protobuf.internal import wire_format
+
+
+# Note that much of this code is ported from //net/proto/ProtocolBuffer, and
+# that the interface is strongly inspired by CodedInputStream from the C++
+# proto2 implementation.
+
+
+class InputStreamBuffer(object):
+
+  """Contains all logic for reading bits, and dealing with stream position.
+
+  If an InputStream method ever raises an exception, the stream is left
+  in an indeterminate state and is not safe for further use.
+  """
+
+  def __init__(self, s):
+    # What we really want is something like array('B', s), where elements we
+    # read from the array are already given to us as one-byte integers.  BUT
+    # using array() instead of buffer() would force full string copies to result
+    # from each GetSubBuffer() call.
+    #
+    # So, if the N serialized bytes of a single protocol buffer object are
+    # split evenly between 2 child messages, and so on recursively, using
+    # array('B', s) instead of buffer() would incur an additional N*logN bytes
+    # copied during deserialization.
+    #
+    # The higher constant overhead of having to ord() for every byte we read
+    # from the buffer in _ReadVarintHelper() could definitely lead to worse
+    # performance in many real-world scenarios, even if the asymptotic
+    # complexity is better.  However, our real answer is that the mythical
+    # Python/C extension module output mode for the protocol compiler will
+    # be blazing-fast and will eliminate most use of this class anyway.
+    self._buffer = buffer(s)
+    self._pos = 0
+
+  def EndOfStream(self):
+    """Returns true iff we're at the end of the stream.
+    If this returns true, then a call to any other InputStream method
+    will raise an exception.
+    """
+    return self._pos >= len(self._buffer)
+
+  def Position(self):
+    """Returns the current position in the stream, or equivalently, the
+    number of bytes read so far.
+    """
+    return self._pos
+
+  def GetSubBuffer(self, size=None):
+    """Returns a sequence-like object that represents a portion of our
+    underlying sequence.
+
+    Position 0 in the returned object corresponds to self.Position()
+    in this stream.
+
+    If size is specified, then the returned object ends after the
+    next "size" bytes in this stream.  If size is not specified,
+    then the returned object ends at the end of this stream.
+
+    We guarantee that the returned object R supports the Python buffer
+    interface (and thus that the call buffer(R) will work).
+
+    Note that the returned buffer is read-only.
+
+    The intended use for this method is for nested-message and nested-group
+    deserialization, where we want to make a recursive MergeFromString()
+    call on the portion of the original sequence that contains the serialized
+    nested message.  (And we'd like to do so without making unnecessary string
+    copies).
+
+    REQUIRES: size is nonnegative.
+    """
+    # Note that buffer() doesn't perform any actual string copy.
+    if size is None:
+      return buffer(self._buffer, self._pos)
+    else:
+      if size < 0:
+        raise message.DecodeError('Negative size %d' % size)
+      return buffer(self._buffer, self._pos, size)
+
+  def SkipBytes(self, num_bytes):
+    """Skip num_bytes bytes ahead, or go to the end of the stream, whichever
+    comes first.
+
+    REQUIRES: num_bytes is nonnegative.
+    """
+    if num_bytes < 0:
+      raise message.DecodeError('Negative num_bytes %d' % num_bytes)
+    self._pos += num_bytes
+    self._pos = min(self._pos, len(self._buffer))
+
+  def ReadBytes(self, size):
+    """Reads up to 'size' bytes from the stream, stopping early
+    only if we reach the end of the stream.  Returns the bytes read
+    as a string.
+    """
+    if size < 0:
+      raise message.DecodeError('Negative size %d' % size)
+    s = (self._buffer[self._pos : self._pos + size])
+    self._pos += len(s)  # Only advance by the number of bytes actually read.
+    return s
+
+  def ReadLittleEndian32(self):
+    """Interprets the next 4 bytes of the stream as a little-endian
+    encoded, unsiged 32-bit integer, and returns that integer.
+    """
+    try:
+      i = struct.unpack(wire_format.FORMAT_UINT32_LITTLE_ENDIAN,
+                        self._buffer[self._pos : self._pos + 4])
+      self._pos += 4
+      return i[0]  # unpack() result is a 1-element tuple.
+    except struct.error, e:
+      raise message.DecodeError(e)
+
+  def ReadLittleEndian64(self):
+    """Interprets the next 8 bytes of the stream as a little-endian
+    encoded, unsiged 64-bit integer, and returns that integer.
+    """
+    try:
+      i = struct.unpack(wire_format.FORMAT_UINT64_LITTLE_ENDIAN,
+                        self._buffer[self._pos : self._pos + 8])
+      self._pos += 8
+      return i[0]  # unpack() result is a 1-element tuple.
+    except struct.error, e:
+      raise message.DecodeError(e)
+
+  def ReadVarint32(self):
+    """Reads a varint from the stream, interprets this varint
+    as a signed, 32-bit integer, and returns the integer.
+    """
+    i = self.ReadVarint64()
+    if not wire_format.INT32_MIN <= i <= wire_format.INT32_MAX:
+      raise message.DecodeError('Value out of range for int32: %d' % i)
+    return int(i)
+
+  def ReadVarUInt32(self):
+    """Reads a varint from the stream, interprets this varint
+    as an unsigned, 32-bit integer, and returns the integer.
+    """
+    i = self.ReadVarUInt64()
+    if i > wire_format.UINT32_MAX:
+      raise message.DecodeError('Value out of range for uint32: %d' % i)
+    return i
+
+  def ReadVarint64(self):
+    """Reads a varint from the stream, interprets this varint
+    as a signed, 64-bit integer, and returns the integer.
+    """
+    i = self.ReadVarUInt64()
+    if i > wire_format.INT64_MAX:
+      i -= (1 << 64)
+    return i
+
+  def ReadVarUInt64(self):
+    """Reads a varint from the stream, interprets this varint
+    as an unsigned, 64-bit integer, and returns the integer.
+    """
+    i = self._ReadVarintHelper()
+    if not 0 <= i <= wire_format.UINT64_MAX:
+      raise message.DecodeError('Value out of range for uint64: %d' % i)
+    return i
+
+  def _ReadVarintHelper(self):
+    """Helper for the various varint-reading methods above.
+    Reads an unsigned, varint-encoded integer from the stream and
+    returns this integer.
+
+    Does no bounds checking except to ensure that we read at most as many bytes
+    as could possibly be present in a varint-encoded 64-bit number.
+    """
+    result = 0
+    shift = 0
+    while 1:
+      if shift >= 64:
+        raise message.DecodeError('Too many bytes when decoding varint.')
+      try:
+        b = ord(self._buffer[self._pos])
+      except IndexError:
+        raise message.DecodeError('Truncated varint.')
+      self._pos += 1
+      result |= ((b & 0x7f) << shift)
+      shift += 7
+      if not (b & 0x80):
+        return result
+
+class InputStreamArray(object):
+  def __init__(self, s):
+    self._buffer = array('B', s)
+    self._pos = 0
+
+  def EndOfStream(self):
+    return self._pos >= len(self._buffer)
+
+  def Position(self):
+    return self._pos
+
+  def GetSubBuffer(self, size=None):
+    if size is None:
+      return self._buffer[self._pos : ].tostring()
+    else:
+      if size < 0:
+        raise message.DecodeError('Negative size %d' % size)
+      return self._buffer[self._pos : self._pos + size].tostring()
+
+  def SkipBytes(self, num_bytes):
+    if num_bytes < 0:
+      raise message.DecodeError('Negative num_bytes %d' % num_bytes)
+    self._pos += num_bytes
+    self._pos = min(self._pos, len(self._buffer))
+
+  def ReadBytes(self, size):
+    if size < 0:
+      raise message.DecodeError('Negative size %d' % size)
+    s = self._buffer[self._pos : self._pos + size].tostring()
+    self._pos += len(s)  # Only advance by the number of bytes actually read.
+    return s
+
+  def ReadLittleEndian32(self):
+    try:
+      i = struct.unpack(wire_format.FORMAT_UINT32_LITTLE_ENDIAN,
+                        self._buffer[self._pos : self._pos + 4])
+      self._pos += 4
+      return i[0]  # unpack() result is a 1-element tuple.
+    except struct.error, e:
+      raise message.DecodeError(e)
+
+  def ReadLittleEndian64(self):
+    try:
+      i = struct.unpack(wire_format.FORMAT_UINT64_LITTLE_ENDIAN,
+                        self._buffer[self._pos : self._pos + 8])
+      self._pos += 8
+      return i[0]  # unpack() result is a 1-element tuple.
+    except struct.error, e:
+      raise message.DecodeError(e)
+
+  def ReadVarint32(self):
+    i = self.ReadVarint64()
+    if not wire_format.INT32_MIN <= i <= wire_format.INT32_MAX:
+      raise message.DecodeError('Value out of range for int32: %d' % i)
+    return int(i)
+
+  def ReadVarUInt32(self):
+    i = self.ReadVarUInt64()
+    if i > wire_format.UINT32_MAX:
+      raise message.DecodeError('Value out of range for uint32: %d' % i)
+    return i
+
+  def ReadVarint64(self):
+    i = self.ReadVarUInt64()
+    if i > wire_format.INT64_MAX:
+      i -= (1 << 64)
+    return i
+
+  def ReadVarUInt64(self):
+    i = self._ReadVarintHelper()
+    if not 0 <= i <= wire_format.UINT64_MAX:
+      raise message.DecodeError('Value out of range for uint64: %d' % i)
+    return i
+
+  def _ReadVarintHelper(self):
+    result = 0
+    shift = 0
+    while 1:
+      if shift >= 64:
+        raise message.DecodeError('Too many bytes when decoding varint.')
+      try:
+        b = self._buffer[self._pos]
+      except IndexError:
+        raise message.DecodeError('Truncated varint.')
+      self._pos += 1
+      result |= ((b & 0x7f) << shift)
+      shift += 7
+      if not (b & 0x80):
+        return result
+
+try:
+  buffer("")
+  InputStream = InputStreamBuffer
+except NotImplementedError:
+  # Google App Engine: dev_appserver.py
+  InputStream = InputStreamArray
+except RuntimeError:
+  # Google App Engine: production
+  InputStream = InputStreamArray
diff --git a/froofle/protobuf/internal/message_listener.py b/froofle/protobuf/internal/message_listener.py
new file mode 100644
index 0000000..4397895
--- /dev/null
+++ b/froofle/protobuf/internal/message_listener.py
@@ -0,0 +1,69 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Defines a listener interface for observing certain
+state transitions on Message objects.
+
+Also defines a null implementation of this interface.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+
+class MessageListener(object):
+
+  """Listens for transitions to nonempty and for invalidations of cached
+  byte sizes.  Meant to be registered via Message._SetListener().
+  """
+
+  def TransitionToNonempty(self):
+    """Called the *first* time that this message becomes nonempty.
+    Implementations are free (but not required) to call this method multiple
+    times after the message has become nonempty.
+    """
+    raise NotImplementedError
+
+  def ByteSizeDirty(self):
+    """Called *every* time the cached byte size value
+    for this object is invalidated (transitions from being
+    "clean" to "dirty").
+    """
+    raise NotImplementedError
+
+
+class NullMessageListener(object):
+
+  """No-op MessageListener implementation."""
+
+  def TransitionToNonempty(self):
+    pass
+
+  def ByteSizeDirty(self):
+    pass
diff --git a/froofle/protobuf/internal/output_stream.py b/froofle/protobuf/internal/output_stream.py
new file mode 100644
index 0000000..f62cd1c
--- /dev/null
+++ b/froofle/protobuf/internal/output_stream.py
@@ -0,0 +1,125 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""OutputStream is the primitive interface for sticking bits on the wire.
+
+All protocol buffer serialization can be expressed in terms of
+the OutputStream primitives provided here.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+import array
+import struct
+from froofle.protobuf import message
+from froofle.protobuf.internal import wire_format
+
+
+
+# Note that much of this code is ported from //net/proto/ProtocolBuffer, and
+# that the interface is strongly inspired by CodedOutputStream from the C++
+# proto2 implementation.
+
+
+class OutputStream(object):
+
+  """Contains all logic for writing bits, and ToString() to get the result."""
+
+  def __init__(self):
+    self._buffer = array.array('B')
+
+  def AppendRawBytes(self, raw_bytes):
+    """Appends raw_bytes to our internal buffer."""
+    self._buffer.fromstring(raw_bytes)
+
+  def AppendLittleEndian32(self, unsigned_value):
+    """Appends an unsigned 32-bit integer to the internal buffer,
+    in little-endian byte order.
+    """
+    if not 0 <= unsigned_value <= wire_format.UINT32_MAX:
+      raise message.EncodeError(
+          'Unsigned 32-bit out of range: %d' % unsigned_value)
+    self._buffer.fromstring(struct.pack(
+        wire_format.FORMAT_UINT32_LITTLE_ENDIAN, unsigned_value))
+
+  def AppendLittleEndian64(self, unsigned_value):
+    """Appends an unsigned 64-bit integer to the internal buffer,
+    in little-endian byte order.
+    """
+    if not 0 <= unsigned_value <= wire_format.UINT64_MAX:
+      raise message.EncodeError(
+          'Unsigned 64-bit out of range: %d' % unsigned_value)
+    self._buffer.fromstring(struct.pack(
+        wire_format.FORMAT_UINT64_LITTLE_ENDIAN, unsigned_value))
+
+  def AppendVarint32(self, value):
+    """Appends a signed 32-bit integer to the internal buffer,
+    encoded as a varint.  (Note that a negative varint32 will
+    always require 10 bytes of space.)
+    """
+    if not wire_format.INT32_MIN <= value <= wire_format.INT32_MAX:
+      raise message.EncodeError('Value out of range: %d' % value)
+    self.AppendVarint64(value)
+
+  def AppendVarUInt32(self, value):
+    """Appends an unsigned 32-bit integer to the internal buffer,
+    encoded as a varint.
+    """
+    if not 0 <= value <= wire_format.UINT32_MAX:
+      raise message.EncodeError('Value out of range: %d' % value)
+    self.AppendVarUInt64(value)
+
+  def AppendVarint64(self, value):
+    """Appends a signed 64-bit integer to the internal buffer,
+    encoded as a varint.
+    """
+    if not wire_format.INT64_MIN <= value <= wire_format.INT64_MAX:
+      raise message.EncodeError('Value out of range: %d' % value)
+    if value < 0:
+      value += (1 << 64)
+    self.AppendVarUInt64(value)
+
+  def AppendVarUInt64(self, unsigned_value):
+    """Appends an unsigned 64-bit integer to the internal buffer,
+    encoded as a varint.
+    """
+    if not 0 <= unsigned_value <= wire_format.UINT64_MAX:
+      raise message.EncodeError('Value out of range: %d' % unsigned_value)
+    while True:
+      bits = unsigned_value & 0x7f
+      unsigned_value >>= 7
+      if not unsigned_value:
+        self._buffer.append(bits)
+        break
+      self._buffer.append(0x80|bits)
+
+  def ToString(self):
+    """Returns a string containing the bytes in our internal buffer."""
+    return self._buffer.tostring()
diff --git a/froofle/protobuf/internal/type_checkers.py b/froofle/protobuf/internal/type_checkers.py
new file mode 100644
index 0000000..aaf7a84
--- /dev/null
+++ b/froofle/protobuf/internal/type_checkers.py
@@ -0,0 +1,268 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Provides type checking routines.
+
+This module defines type checking utilities in the forms of dictionaries:
+
+VALUE_CHECKERS: A dictionary of field types and a value validation object.
+TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing
+  function.
+TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization
+  function.
+FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their
+  coresponding wire types.
+TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization
+  function.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+from froofle.protobuf.internal import decoder
+from froofle.protobuf.internal import encoder
+from froofle.protobuf.internal import wire_format
+from froofle.protobuf import descriptor
+
+_FieldDescriptor = descriptor.FieldDescriptor
+
+
+def GetTypeChecker(cpp_type, field_type):
+  """Returns a type checker for a message field of the specified types.
+
+  Args:
+    cpp_type: C++ type of the field (see descriptor.py).
+    field_type: Protocol message field type (see descriptor.py).
+
+  Returns:
+    An instance of TypeChecker which can be used to verify the types
+    of values assigned to a field of the specified type.
+  """
+  if (cpp_type == _FieldDescriptor.CPPTYPE_STRING and
+      field_type == _FieldDescriptor.TYPE_STRING):
+    return UnicodeValueChecker()
+  return _VALUE_CHECKERS[cpp_type]
+
+
+# None of the typecheckers below make any attempt to guard against people
+# subclassing builtin types and doing weird things.  We're not trying to
+# protect against malicious clients here, just people accidentally shooting
+# themselves in the foot in obvious ways.
+
+class TypeChecker(object):
+
+  """Type checker used to catch type errors as early as possible
+  when the client is setting scalar fields in protocol messages.
+  """
+
+  def __init__(self, *acceptable_types):
+    self._acceptable_types = acceptable_types
+
+  def CheckValue(self, proposed_value):
+    if not isinstance(proposed_value, self._acceptable_types):
+      message = ('%.1024r has type %s, but expected one of: %s' %
+                 (proposed_value, type(proposed_value), self._acceptable_types))
+      raise TypeError(message)
+
+
+# IntValueChecker and its subclasses perform integer type-checks
+# and bounds-checks.
+class IntValueChecker(object):
+
+  """Checker used for integer fields.  Performs type-check and range check."""
+
+  def CheckValue(self, proposed_value):
+    if not isinstance(proposed_value, (int, long)):
+      message = ('%.1024r has type %s, but expected one of: %s' %
+                 (proposed_value, type(proposed_value), (int, long)))
+      raise TypeError(message)
+    if not self._MIN <= proposed_value <= self._MAX:
+      raise ValueError('Value out of range: %d' % proposed_value)
+
+
+class UnicodeValueChecker(object):
+
+  """Checker used for string fields."""
+
+  def CheckValue(self, proposed_value):
+    if not isinstance(proposed_value, (str, unicode)):
+      message = ('%.1024r has type %s, but expected one of: %s' %
+                 (proposed_value, type(proposed_value), (str, unicode)))
+      raise TypeError(message)
+
+    # If the value is of type 'str' make sure that it is in 7-bit ASCII
+    # encoding.
+    if isinstance(proposed_value, str):
+      try:
+        unicode(proposed_value, 'ascii')
+      except UnicodeDecodeError:
+        raise ValueError('%.1024r isn\'t in 7-bit ASCII encoding.'
+                         % (proposed_value))
+
+
+class Int32ValueChecker(IntValueChecker):
+  # We're sure to use ints instead of longs here since comparison may be more
+  # efficient.
+  _MIN = -2147483648
+  _MAX = 2147483647
+
+
+class Uint32ValueChecker(IntValueChecker):
+  _MIN = 0
+  _MAX = (1 << 32) - 1
+
+
+class Int64ValueChecker(IntValueChecker):
+  _MIN = -(1 << 63)
+  _MAX = (1 << 63) - 1
+
+
+class Uint64ValueChecker(IntValueChecker):
+  _MIN = 0
+  _MAX = (1 << 64) - 1
+
+
+# Type-checkers for all scalar CPPTYPEs.
+_VALUE_CHECKERS = {
+    _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(),
+    _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(),
+    _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(),
+    _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(),
+    _FieldDescriptor.CPPTYPE_DOUBLE: TypeChecker(
+        float, int, long),
+    _FieldDescriptor.CPPTYPE_FLOAT: TypeChecker(
+        float, int, long),
+    _FieldDescriptor.CPPTYPE_BOOL: TypeChecker(bool, int),
+    _FieldDescriptor.CPPTYPE_ENUM: Int32ValueChecker(),
+    _FieldDescriptor.CPPTYPE_STRING: TypeChecker(str),
+    }
+
+
+# Map from field type to a function F, such that F(field_num, value)
+# gives the total byte size for a value of the given type.  This
+# byte size includes tag information and any other additional space
+# associated with serializing "value".
+TYPE_TO_BYTE_SIZE_FN = {
+    _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize,
+    _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize,
+    _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize,
+    _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize,
+    _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize,
+    _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize,
+    _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize,
+    _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize,
+    _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize,
+    _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize,
+    _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize,
+    _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize,
+    _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize,
+    _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize,
+    _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize,
+    _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize,
+    _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize,
+    _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize
+    }
+
+
+# Maps from field type to an unbound Encoder method F, such that
+# F(encoder, field_number, value) will append the serialization
+# of a value of this type to the encoder.
+_Encoder = encoder.Encoder
+TYPE_TO_SERIALIZE_METHOD = {
+    _FieldDescriptor.TYPE_DOUBLE: _Encoder.AppendDouble,
+    _FieldDescriptor.TYPE_FLOAT: _Encoder.AppendFloat,
+    _FieldDescriptor.TYPE_INT64: _Encoder.AppendInt64,
+    _FieldDescriptor.TYPE_UINT64: _Encoder.AppendUInt64,
+    _FieldDescriptor.TYPE_INT32: _Encoder.AppendInt32,
+    _FieldDescriptor.TYPE_FIXED64: _Encoder.AppendFixed64,
+    _FieldDescriptor.TYPE_FIXED32: _Encoder.AppendFixed32,
+    _FieldDescriptor.TYPE_BOOL: _Encoder.AppendBool,
+    _FieldDescriptor.TYPE_STRING: _Encoder.AppendString,
+    _FieldDescriptor.TYPE_GROUP: _Encoder.AppendGroup,
+    _FieldDescriptor.TYPE_MESSAGE: _Encoder.AppendMessage,
+    _FieldDescriptor.TYPE_BYTES: _Encoder.AppendBytes,
+    _FieldDescriptor.TYPE_UINT32: _Encoder.AppendUInt32,
+    _FieldDescriptor.TYPE_ENUM: _Encoder.AppendEnum,
+    _FieldDescriptor.TYPE_SFIXED32: _Encoder.AppendSFixed32,
+    _FieldDescriptor.TYPE_SFIXED64: _Encoder.AppendSFixed64,
+    _FieldDescriptor.TYPE_SINT32: _Encoder.AppendSInt32,
+    _FieldDescriptor.TYPE_SINT64: _Encoder.AppendSInt64,
+    }
+
+
+# Maps from field type to expected wiretype.
+FIELD_TYPE_TO_WIRE_TYPE = {
+    _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64,
+    _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32,
+    _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64,
+    _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32,
+    _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_STRING:
+      wire_format.WIRETYPE_LENGTH_DELIMITED,
+    _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP,
+    _FieldDescriptor.TYPE_MESSAGE:
+      wire_format.WIRETYPE_LENGTH_DELIMITED,
+    _FieldDescriptor.TYPE_BYTES:
+      wire_format.WIRETYPE_LENGTH_DELIMITED,
+    _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32,
+    _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64,
+    _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT,
+    _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT,
+    }
+
+
+# Maps from field type to an unbound Decoder method F,
+# such that F(decoder) will read a field of the requested type.
+#
+# Note that Message and Group are intentionally missing here.
+# They're handled by _RecursivelyMerge().
+_Decoder = decoder.Decoder
+TYPE_TO_DESERIALIZE_METHOD = {
+    _FieldDescriptor.TYPE_DOUBLE: _Decoder.ReadDouble,
+    _FieldDescriptor.TYPE_FLOAT: _Decoder.ReadFloat,
+    _FieldDescriptor.TYPE_INT64: _Decoder.ReadInt64,
+    _FieldDescriptor.TYPE_UINT64: _Decoder.ReadUInt64,
+    _FieldDescriptor.TYPE_INT32: _Decoder.ReadInt32,
+    _FieldDescriptor.TYPE_FIXED64: _Decoder.ReadFixed64,
+    _FieldDescriptor.TYPE_FIXED32: _Decoder.ReadFixed32,
+    _FieldDescriptor.TYPE_BOOL: _Decoder.ReadBool,
+    _FieldDescriptor.TYPE_STRING: _Decoder.ReadString,
+    _FieldDescriptor.TYPE_BYTES: _Decoder.ReadBytes,
+    _FieldDescriptor.TYPE_UINT32: _Decoder.ReadUInt32,
+    _FieldDescriptor.TYPE_ENUM: _Decoder.ReadEnum,
+    _FieldDescriptor.TYPE_SFIXED32: _Decoder.ReadSFixed32,
+    _FieldDescriptor.TYPE_SFIXED64: _Decoder.ReadSFixed64,
+    _FieldDescriptor.TYPE_SINT32: _Decoder.ReadSInt32,
+    _FieldDescriptor.TYPE_SINT64: _Decoder.ReadSInt64,
+    }
diff --git a/froofle/protobuf/internal/wire_format.py b/froofle/protobuf/internal/wire_format.py
new file mode 100644
index 0000000..4d823c8
--- /dev/null
+++ b/froofle/protobuf/internal/wire_format.py
@@ -0,0 +1,236 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Constants and static functions to support protocol buffer wire format."""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+import struct
+from froofle.protobuf import message
+
+
+TAG_TYPE_BITS = 3  # Number of bits used to hold type info in a proto tag.
+_TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1  # 0x7
+
+# These numbers identify the wire type of a protocol buffer value.
+# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded
+# tag-and-type to store one of these WIRETYPE_* constants.
+# These values must match WireType enum in //net/proto2/public/wire_format.h.
+WIRETYPE_VARINT = 0
+WIRETYPE_FIXED64 = 1
+WIRETYPE_LENGTH_DELIMITED = 2
+WIRETYPE_START_GROUP = 3
+WIRETYPE_END_GROUP = 4
+WIRETYPE_FIXED32 = 5
+_WIRETYPE_MAX = 5
+
+
+# Bounds for various integer types.
+INT32_MAX = int((1 << 31) - 1)
+INT32_MIN = int(-(1 << 31))
+UINT32_MAX = (1 << 32) - 1
+
+INT64_MAX = (1 << 63) - 1
+INT64_MIN = -(1 << 63)
+UINT64_MAX = (1 << 64) - 1
+
+# "struct" format strings that will encode/decode the specified formats.
+FORMAT_UINT32_LITTLE_ENDIAN = '<I'
+FORMAT_UINT64_LITTLE_ENDIAN = '<Q'
+
+
+# We'll have to provide alternate implementations of AppendLittleEndian*() on
+# any architectures where these checks fail.
+if struct.calcsize(FORMAT_UINT32_LITTLE_ENDIAN) != 4:
+  raise AssertionError('Format "I" is not a 32-bit number.')
+if struct.calcsize(FORMAT_UINT64_LITTLE_ENDIAN) != 8:
+  raise AssertionError('Format "Q" is not a 64-bit number.')
+
+
+def PackTag(field_number, wire_type):
+  """Returns an unsigned 32-bit integer that encodes the field number and
+  wire type information in standard protocol message wire format.
+
+  Args:
+    field_number: Expected to be an integer in the range [1, 1 << 29)
+    wire_type: One of the WIRETYPE_* constants.
+  """
+  if not 0 <= wire_type <= _WIRETYPE_MAX:
+    raise message.EncodeError('Unknown wire type: %d' % wire_type)
+  return (field_number << TAG_TYPE_BITS) | wire_type
+
+
+def UnpackTag(tag):
+  """The inverse of PackTag().  Given an unsigned 32-bit number,
+  returns a (field_number, wire_type) tuple.
+  """
+  return (tag >> TAG_TYPE_BITS), (tag & _TAG_TYPE_MASK)
+
+
+def ZigZagEncode(value):
+  """ZigZag Transform:  Encodes signed integers so that they can be
+  effectively used with varint encoding.  See wire_format.h for
+  more details.
+  """
+  if value >= 0:
+    return value << 1
+  return (value << 1) ^ (~0)
+
+
+def ZigZagDecode(value):
+  """Inverse of ZigZagEncode()."""
+  if not value & 0x1:
+    return value >> 1
+  return (value >> 1) ^ (~0)
+
+
+
+# The *ByteSize() functions below return the number of bytes required to
+# serialize "field number + type" information and then serialize the value.
+
+
+def Int32ByteSize(field_number, int32):
+  return Int64ByteSize(field_number, int32)
+
+
+def Int64ByteSize(field_number, int64):
+  # Have to convert to uint before calling UInt64ByteSize().
+  return UInt64ByteSize(field_number, 0xffffffffffffffff & int64)
+
+
+def UInt32ByteSize(field_number, uint32):
+  return UInt64ByteSize(field_number, uint32)
+
+
+def UInt64ByteSize(field_number, uint64):
+  return _TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64)
+
+
+def SInt32ByteSize(field_number, int32):
+  return UInt32ByteSize(field_number, ZigZagEncode(int32))
+
+
+def SInt64ByteSize(field_number, int64):
+  return UInt64ByteSize(field_number, ZigZagEncode(int64))
+
+
+def Fixed32ByteSize(field_number, fixed32):
+  return _TagByteSize(field_number) + 4
+
+
+def Fixed64ByteSize(field_number, fixed64):
+  return _TagByteSize(field_number) + 8
+
+
+def SFixed32ByteSize(field_number, sfixed32):
+  return _TagByteSize(field_number) + 4
+
+
+def SFixed64ByteSize(field_number, sfixed64):
+  return _TagByteSize(field_number) + 8
+
+
+def FloatByteSize(field_number, flt):
+  return _TagByteSize(field_number) + 4
+
+
+def DoubleByteSize(field_number, double):
+  return _TagByteSize(field_number) + 8
+
+
+def BoolByteSize(field_number, b):
+  return _TagByteSize(field_number) + 1
+
+
+def EnumByteSize(field_number, enum):
+  return UInt32ByteSize(field_number, enum)
+
+
+def StringByteSize(field_number, string):
+  return BytesByteSize(field_number, string.encode('utf-8'))
+
+
+def BytesByteSize(field_number, b):
+  return (_TagByteSize(field_number)
+          + _VarUInt64ByteSizeNoTag(len(b))
+          + len(b))
+
+
+def GroupByteSize(field_number, message):
+  return (2 * _TagByteSize(field_number)  # START and END group.
+          + message.ByteSize())
+
+
+def MessageByteSize(field_number, message):
+  return (_TagByteSize(field_number)
+          + _VarUInt64ByteSizeNoTag(message.ByteSize())
+          + message.ByteSize())
+
+
+def MessageSetItemByteSize(field_number, msg):
+  # First compute the sizes of the tags.
+  # There are 2 tags for the beginning and ending of the repeated group, that
+  # is field number 1, one with field number 2 (type_id) and one with field
+  # number 3 (message).
+  total_size = (2 * _TagByteSize(1) + _TagByteSize(2) + _TagByteSize(3))
+
+  # Add the number of bytes for type_id.
+  total_size += _VarUInt64ByteSizeNoTag(field_number)
+
+  message_size = msg.ByteSize()
+
+  # The number of bytes for encoding the length of the message.
+  total_size += _VarUInt64ByteSizeNoTag(message_size)
+
+  # The size of the message.
+  total_size += message_size
+  return total_size
+
+
+# Private helper functions for the *ByteSize() functions above.
+
+
+def _TagByteSize(field_number):
+  """Returns the bytes required to serialize a tag with this field number."""
+  # Just pass in type 0, since the type won't affect the tag+type size.
+  return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0))
+
+
+def _VarUInt64ByteSizeNoTag(uint64):
+  """Returns the bytes required to serialize a single varint.
+  uint64 must be unsigned.
+  """
+  if uint64 > UINT64_MAX:
+    raise message.EncodeError('Value out of range: %d' % uint64)
+  bytes = 1
+  while uint64 > 0x7f:
+    bytes += 1
+    uint64 >>= 7
+  return bytes
diff --git a/froofle/protobuf/message.py b/froofle/protobuf/message.py
new file mode 100644
index 0000000..ed71485
--- /dev/null
+++ b/froofle/protobuf/message.py
@@ -0,0 +1,246 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+# TODO(robinson): We should just make these methods all "pure-virtual" and move
+# all implementation out, into reflection.py for now.
+
+
+"""Contains an abstract base class for protocol messages."""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+from froofle.protobuf import text_format
+
+class Error(Exception): pass
+class DecodeError(Error): pass
+class EncodeError(Error): pass
+
+
+class Message(object):
+
+  """Abstract base class for protocol messages.
+
+  Protocol message classes are almost always generated by the protocol
+  compiler.  These generated types subclass Message and implement the methods
+  shown below.
+
+  TODO(robinson): Link to an HTML document here.
+
+  TODO(robinson): Document that instances of this class will also
+  have an Extensions attribute with __getitem__ and __setitem__.
+  Again, not sure how to best convey this.
+
+  TODO(robinson): Document that the class must also have a static
+    RegisterExtension(extension_field) method.
+    Not sure how to best express at this point.
+  """
+
+  # TODO(robinson): Document these fields and methods.
+
+  __slots__ = []
+
+  DESCRIPTOR = None
+
+  def __eq__(self, other_msg):
+    raise NotImplementedError
+
+  def __ne__(self, other_msg):
+    # Can't just say self != other_msg, since that would infinitely recurse. :)
+    return not self == other_msg
+
+  def __str__(self):
+    return text_format.MessageToString(self)
+
+  def MergeFrom(self, other_msg):
+    """Merges the contents of the specified message into current message.
+
+    This method merges the contents of the specified message into the current
+    message. Singular fields that are set in the specified message overwrite
+    the corresponding fields in the current message. Repeated fields are
+    appended. Singular sub-messages and groups are recursively merged.
+
+    Args:
+      other_msg: Message to merge into the current message.
+    """
+    raise NotImplementedError
+
+  def CopyFrom(self, other_msg):
+    """Copies the content of the specified message into the current message.
+
+    The method clears the current message and then merges the specified
+    message using MergeFrom.
+
+    Args:
+      other_msg: Message to copy into the current one.
+    """
+    if self == other_msg:
+      return
+    self.Clear()
+    self.MergeFrom(other_msg)
+
+  def Clear(self):
+    """Clears all data that was set in the message."""
+    raise NotImplementedError
+
+  def IsInitialized(self):
+    """Checks if the message is initialized.
+
+    Returns:
+      The method returns True if the message is initialized (i.e. all of its
+      required fields are set).
+    """
+    raise NotImplementedError
+
+  # TODO(robinson): MergeFromString() should probably return None and be
+  # implemented in terms of a helper that returns the # of bytes read.  Our
+  # deserialization routines would use the helper when recursively
+  # deserializing, but the end user would almost always just want the no-return
+  # MergeFromString().
+
+  def MergeFromString(self, serialized):
+    """Merges serialized protocol buffer data into this message.
+
+    When we find a field in |serialized| that is already present
+    in this message:
+      - If it's a "repeated" field, we append to the end of our list.
+      - Else, if it's a scalar, we overwrite our field.
+      - Else, (it's a nonrepeated composite), we recursively merge
+        into the existing composite.
+
+    TODO(robinson): Document handling of unknown fields.
+
+    Args:
+      serialized: Any object that allows us to call buffer(serialized)
+        to access a string of bytes using the buffer interface.
+
+    TODO(robinson): When we switch to a helper, this will return None.
+
+    Returns:
+      The number of bytes read from |serialized|.
+      For non-group messages, this will always be len(serialized),
+      but for messages which are actually groups, this will
+      generally be less than len(serialized), since we must
+      stop when we reach an END_GROUP tag.  Note that if
+      we *do* stop because of an END_GROUP tag, the number
+      of bytes returned does not include the bytes
+      for the END_GROUP tag information.
+    """
+    raise NotImplementedError
+
+  def ParseFromString(self, serialized):
+    """Like MergeFromString(), except we clear the object first."""
+    self.Clear()
+    self.MergeFromString(serialized)
+
+  def SerializeToString(self):
+    """Serializes the protocol message to a binary string.
+
+    Returns:
+      A binary string representation of the message if all of the required
+      fields in the message are set (i.e. the message is initialized).
+
+    Raises:
+      message.EncodeError if the message isn't initialized.
+    """
+    raise NotImplementedError
+
+  def SerializePartialToString(self):
+    """Serializes the protocol message to a binary string.
+
+    This method is similar to SerializeToString but doesn't check if the
+    message is initialized.
+
+    Returns:
+      A string representation of the partial message.
+    """
+    raise NotImplementedError
+
+  # TODO(robinson): Decide whether we like these better
+  # than auto-generated has_foo() and clear_foo() methods
+  # on the instances themselves.  This way is less consistent
+  # with C++, but it makes reflection-type access easier and
+  # reduces the number of magically autogenerated things.
+  #
+  # TODO(robinson): Be sure to document (and test) exactly
+  # which field names are accepted here.  Are we case-sensitive?
+  # What do we do with fields that share names with Python keywords
+  # like 'lambda' and 'yield'?
+  #
+  # nnorwitz says:
+  # """
+  # Typically (in python), an underscore is appended to names that are
+  # keywords. So they would become lambda_ or yield_.
+  # """
+  def ListFields(self, field_name):
+    """Returns a list of (FieldDescriptor, value) tuples for all
+    fields in the message which are not empty.  A singular field is non-empty
+    if HasField() would return true, and a repeated field is non-empty if
+    it contains at least one element.  The fields are ordered by field
+    number"""
+    raise NotImplementedError
+
+  def HasField(self, field_name):
+    raise NotImplementedError
+
+  def ClearField(self, field_name):
+    raise NotImplementedError
+
+  def HasExtension(self, extension_handle):
+    raise NotImplementedError
+
+  def ClearExtension(self, extension_handle):
+    raise NotImplementedError
+
+  def ByteSize(self):
+    """Returns the serialized size of this message.
+    Recursively calls ByteSize() on all contained messages.
+    """
+    raise NotImplementedError
+
+  def _SetListener(self, message_listener):
+    """Internal method used by the protocol message implementation.
+    Clients should not call this directly.
+
+    Sets a listener that this message will call on certain state transitions.
+
+    The purpose of this method is to register back-edges from children to
+    parents at runtime, for the purpose of setting "has" bits and
+    byte-size-dirty bits in the parent and ancestor objects whenever a child or
+    descendant object is modified.
+
+    If the client wants to disconnect this Message from the object tree, she
+    explicitly sets callback to None.
+
+    If message_listener is None, unregisters any existing listener.  Otherwise,
+    message_listener must implement the MessageListener interface in
+    internal/message_listener.py, and we discard any listener registered
+    via a previous _SetListener() call.
+    """
+    raise NotImplementedError
diff --git a/froofle/protobuf/reflection.py b/froofle/protobuf/reflection.py
new file mode 100644
index 0000000..e2abff0
--- /dev/null
+++ b/froofle/protobuf/reflection.py
@@ -0,0 +1,1653 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+# This code is meant to work on Python 2.4 and above only.
+#
+# TODO(robinson): Helpers for verbose, common checks like seeing if a
+# descriptor's cpp_type is CPPTYPE_MESSAGE.
+
+"""Contains a metaclass and helper functions used to create
+protocol message classes from Descriptor objects at runtime.
+
+Recall that a metaclass is the "type" of a class.
+(A class is to a metaclass what an instance is to a class.)
+
+In this case, we use the GeneratedProtocolMessageType metaclass
+to inject all the useful functionality into the classes
+output by the protocol compiler at compile-time.
+
+The upshot of all this is that the real implementation
+details for ALL pure-Python protocol buffers are *here in
+this file*.
+"""
+
+__author__ = 'robinson@google.com (Will Robinson)'
+
+import heapq
+import threading
+import weakref
+# We use "as" to avoid name collisions with variables.
+from froofle.protobuf.internal import decoder
+from froofle.protobuf.internal import encoder
+from froofle.protobuf.internal import message_listener as message_listener_mod
+from froofle.protobuf.internal import type_checkers
+from froofle.protobuf.internal import wire_format
+from froofle.protobuf import descriptor as descriptor_mod
+from froofle.protobuf import message as message_mod
+
+_FieldDescriptor = descriptor_mod.FieldDescriptor
+
+
+class GeneratedProtocolMessageType(type):
+
+  """Metaclass for protocol message classes created at runtime from Descriptors.
+
+  We add implementations for all methods described in the Message class.  We
+  also create properties to allow getting/setting all fields in the protocol
+  message.  Finally, we create slots to prevent users from accidentally
+  "setting" nonexistent fields in the protocol message, which then wouldn't get
+  serialized / deserialized properly.
+
+  The protocol compiler currently uses this metaclass to create protocol
+  message classes at runtime.  Clients can also manually create their own
+  classes at runtime, as in this example:
+
+  mydescriptor = Descriptor(.....)
+  class MyProtoClass(Message):
+    __metaclass__ = GeneratedProtocolMessageType
+    DESCRIPTOR = mydescriptor
+  myproto_instance = MyProtoClass()
+  myproto.foo_field = 23
+  ...
+  """
+
+  # Must be consistent with the protocol-compiler code in
+  # proto2/compiler/internal/generator.*.
+  _DESCRIPTOR_KEY = 'DESCRIPTOR'
+
+  def __new__(cls, name, bases, dictionary):
+    """Custom allocation for runtime-generated class types.
+
+    We override __new__ because this is apparently the only place
+    where we can meaningfully set __slots__ on the class we're creating(?).
+    (The interplay between metaclasses and slots is not very well-documented).
+
+    Args:
+      name: Name of the class (ignored, but required by the
+        metaclass protocol).
+      bases: Base classes of the class we're constructing.
+        (Should be message.Message).  We ignore this field, but
+        it's required by the metaclass protocol
+      dictionary: The class dictionary of the class we're
+        constructing.  dictionary[_DESCRIPTOR_KEY] must contain
+        a Descriptor object describing this protocol message
+        type.
+
+    Returns:
+      Newly-allocated class.
+    """
+    descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY]
+    _AddSlots(descriptor, dictionary)
+    _AddClassAttributesForNestedExtensions(descriptor, dictionary)
+    superclass = super(GeneratedProtocolMessageType, cls)
+    return superclass.__new__(cls, name, bases, dictionary)
+
+  def __init__(cls, name, bases, dictionary):
+    """Here we perform the majority of our work on the class.
+    We add enum getters, an __init__ method, implementations
+    of all Message methods, and properties for all fields
+    in the protocol type.
+
+    Args:
+      name: Name of the class (ignored, but required by the
+        metaclass protocol).
+      bases: Base classes of the class we're constructing.
+        (Should be message.Message).  We ignore this field, but
+        it's required by the metaclass protocol
+      dictionary: The class dictionary of the class we're
+        constructing.  dictionary[_DESCRIPTOR_KEY] must contain
+        a Descriptor object describing this protocol message
+        type.
+    """
+    descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY]
+    # We act as a "friend" class of the descriptor, setting
+    # its _concrete_class attribute the first time we use a
+    # given descriptor to initialize a concrete protocol message
+    # class.
+    concrete_class_attr_name = '_concrete_class'
+    if not hasattr(descriptor, concrete_class_attr_name):
+      setattr(descriptor, concrete_class_attr_name, cls)
+    cls._known_extensions = []
+    _AddEnumValues(descriptor, cls)
+    _AddInitMethod(descriptor, cls)
+    _AddPropertiesForFields(descriptor, cls)
+    _AddStaticMethods(cls)
+    _AddMessageMethods(descriptor, cls)
+    _AddPrivateHelperMethods(cls)
+    superclass = super(GeneratedProtocolMessageType, cls)
+    superclass.__init__(cls, name, bases, dictionary)
+
+
+# Stateless helpers for GeneratedProtocolMessageType below.
+# Outside clients should not access these directly.
+#
+# I opted not to make any of these methods on the metaclass, to make it more
+# clear that I'm not really using any state there and to keep clients from
+# thinking that they have direct access to these construction helpers.
+
+
+def _PropertyName(proto_field_name):
+  """Returns the name of the public property attribute which
+  clients can use to get and (in some cases) set the value
+  of a protocol message field.
+
+  Args:
+    proto_field_name: The protocol message field name, exactly
+      as it appears (or would appear) in a .proto file.
+  """
+  # TODO(robinson): Escape Python keywords (e.g., yield), and test this support.
+  # nnorwitz makes my day by writing:
+  # """
+  # FYI.  See the keyword module in the stdlib. This could be as simple as:
+  #
+  # if keyword.iskeyword(proto_field_name):
+  #   return proto_field_name + "_"
+  # return proto_field_name
+  # """
+  return proto_field_name
+
+
+def _ValueFieldName(proto_field_name):
+  """Returns the name of the (internal) instance attribute which objects
+  should use to store the current value for a given protocol message field.
+
+  Args:
+    proto_field_name: The protocol message field name, exactly
+      as it appears (or would appear) in a .proto file.
+  """
+  return '_value_' + proto_field_name
+
+
+def _HasFieldName(proto_field_name):
+  """Returns the name of the (internal) instance attribute which
+  objects should use to store a boolean telling whether this field
+  is explicitly set or not.
+
+  Args:
+    proto_field_name: The protocol message field name, exactly
+      as it appears (or would appear) in a .proto file.
+  """
+  return '_has_' + proto_field_name
+
+
+def _AddSlots(message_descriptor, dictionary):
+  """Adds a __slots__ entry to dictionary, containing the names of all valid
+  attributes for this message type.
+
+  Args:
+    message_descriptor: A Descriptor instance describing this message type.
+    dictionary: Class dictionary to which we'll add a '__slots__' entry.
+  """
+  field_names = [_ValueFieldName(f.name) for f in message_descriptor.fields]
+  field_names.extend(_HasFieldName(f.name) for f in message_descriptor.fields
+                     if f.label != _FieldDescriptor.LABEL_REPEATED)
+  field_names.extend(('Extensions',
+                      '_cached_byte_size',
+                      '_cached_byte_size_dirty',
+                      '_called_transition_to_nonempty',
+                      '_listener',
+                      '_lock', '__weakref__'))
+  dictionary['__slots__'] = field_names
+
+
+def _AddClassAttributesForNestedExtensions(descriptor, dictionary):
+  extension_dict = descriptor.extensions_by_name
+  for extension_name, extension_field in extension_dict.iteritems():
+    assert extension_name not in dictionary
+    dictionary[extension_name] = extension_field
+
+
+def _AddEnumValues(descriptor, cls):
+  """Sets class-level attributes for all enum fields defined in this message.
+
+  Args:
+    descriptor: Descriptor object for this message type.
+    cls: Class we're constructing for this message type.
+  """
+  for enum_type in descriptor.enum_types:
+    for enum_value in enum_type.values:
+      setattr(cls, enum_value.name, enum_value.number)
+
+
+def _DefaultValueForField(message, field):
+  """Returns a default value for a field.
+
+  Args:
+    message: Message instance containing this field, or a weakref proxy
+      of same.
+    field: FieldDescriptor object for this field.
+
+  Returns: A default value for this field.  May refer back to |message|
+    via a weak reference.
+  """
+  # TODO(robinson): Only the repeated fields need a reference to 'message' (so
+  # that they can set the 'has' bit on the containing Message when someone
+  # append()s a value).  We could special-case this, and avoid an extra
+  # function call on __init__() and Clear() for non-repeated fields.
+
+  # TODO(robinson): Find a better place for the default value assertion in this
+  # function.  No need to repeat them every time the client calls Clear('foo').
+  # (We should probably just assert these things once and as early as possible,
+  # by tightening checking in the descriptor classes.)
+  if field.label == _FieldDescriptor.LABEL_REPEATED:
+    if field.default_value != []:
+      raise ValueError('Repeated field default value not empty list: %s' % (
+          field.default_value))
+    listener = _Listener(message, None)
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      # We can't look at _concrete_class yet since it might not have
+      # been set.  (Depends on order in which we initialize the classes).
+      return _RepeatedCompositeFieldContainer(listener, field.message_type)
+    else:
+      return _RepeatedScalarFieldContainer(
+          listener, type_checkers.GetTypeChecker(field.cpp_type, field.type))
+
+  if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+    assert field.default_value is None
+
+  return field.default_value
+
+
+def _AddInitMethod(message_descriptor, cls):
+  """Adds an __init__ method to cls."""
+  fields = message_descriptor.fields
+  def init(self):
+    self._cached_byte_size = 0
+    self._cached_byte_size_dirty = False
+    self._listener = message_listener_mod.NullMessageListener()
+    self._called_transition_to_nonempty = False
+    # TODO(robinson): We should only create a lock if we really need one
+    # in this class.
+    self._lock = threading.Lock()
+    for field in fields:
+      default_value = _DefaultValueForField(self, field)
+      python_field_name = _ValueFieldName(field.name)
+      setattr(self, python_field_name, default_value)
+      if field.label != _FieldDescriptor.LABEL_REPEATED:
+        setattr(self, _HasFieldName(field.name), False)
+    self.Extensions = _ExtensionDict(self, cls._known_extensions)
+
+  init.__module__ = None
+  init.__doc__ = None
+  cls.__init__ = init
+
+
+def _AddPropertiesForFields(descriptor, cls):
+  """Adds properties for all fields in this protocol message type."""
+  for field in descriptor.fields:
+    _AddPropertiesForField(field, cls)
+
+
+def _AddPropertiesForField(field, cls):
+  """Adds a public property for a protocol message field.
+  Clients can use this property to get and (in the case
+  of non-repeated scalar fields) directly set the value
+  of a protocol message field.
+
+  Args:
+    field: A FieldDescriptor for this field.
+    cls: The class we're constructing.
+  """
+  # Catch it if we add other types that we should
+  # handle specially here.
+  assert _FieldDescriptor.MAX_CPPTYPE == 10
+
+  if field.label == _FieldDescriptor.LABEL_REPEATED:
+    _AddPropertiesForRepeatedField(field, cls)
+  elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+    _AddPropertiesForNonRepeatedCompositeField(field, cls)
+  else:
+    _AddPropertiesForNonRepeatedScalarField(field, cls)
+
+
+def _AddPropertiesForRepeatedField(field, cls):
+  """Adds a public property for a "repeated" protocol message field.  Clients
+  can use this property to get the value of the field, which will be either a
+  _RepeatedScalarFieldContainer or _RepeatedCompositeFieldContainer (see
+  below).
+
+  Note that when clients add values to these containers, we perform
+  type-checking in the case of repeated scalar fields, and we also set any
+  necessary "has" bits as a side-effect.
+
+  Args:
+    field: A FieldDescriptor for this field.
+    cls: The class we're constructing.
+  """
+  proto_field_name = field.name
+  python_field_name = _ValueFieldName(proto_field_name)
+  property_name = _PropertyName(proto_field_name)
+
+  def getter(self):
+    return getattr(self, python_field_name)
+  getter.__module__ = None
+  getter.__doc__ = 'Getter for %s.' % proto_field_name
+
+  # We define a setter just so we can throw an exception with a more
+  # helpful error message.
+  def setter(self, new_value):
+    raise AttributeError('Assignment not allowed to repeated field '
+                         '"%s" in protocol message object.' % proto_field_name)
+
+  doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name
+  setattr(cls, property_name, property(getter, setter, doc=doc))
+
+
+def _AddPropertiesForNonRepeatedScalarField(field, cls):
+  """Adds a public property for a nonrepeated, scalar protocol message field.
+  Clients can use this property to get and directly set the value of the field.
+  Note that when the client sets the value of a field by using this property,
+  all necessary "has" bits are set as a side-effect, and we also perform
+  type-checking.
+
+  Args:
+    field: A FieldDescriptor for this field.
+    cls: The class we're constructing.
+  """
+  proto_field_name = field.name
+  python_field_name = _ValueFieldName(proto_field_name)
+  has_field_name = _HasFieldName(proto_field_name)
+  property_name = _PropertyName(proto_field_name)
+  type_checker = type_checkers.GetTypeChecker(field.cpp_type, field.type)
+
+  def getter(self):
+    return getattr(self, python_field_name)
+  getter.__module__ = None
+  getter.__doc__ = 'Getter for %s.' % proto_field_name
+  def setter(self, new_value):
+    type_checker.CheckValue(new_value)
+    setattr(self, has_field_name, True)
+    self._MarkByteSizeDirty()
+    self._MaybeCallTransitionToNonemptyCallback()
+    setattr(self, python_field_name, new_value)
+  setter.__module__ = None
+  setter.__doc__ = 'Setter for %s.' % proto_field_name
+
+  # Add a property to encapsulate the getter/setter.
+  doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name
+  setattr(cls, property_name, property(getter, setter, doc=doc))
+
+
+def _AddPropertiesForNonRepeatedCompositeField(field, cls):
+  """Adds a public property for a nonrepeated, composite protocol message field.
+  A composite field is a "group" or "message" field.
+
+  Clients can use this property to get the value of the field, but cannot
+  assign to the property directly.
+
+  Args:
+    field: A FieldDescriptor for this field.
+    cls: The class we're constructing.
+  """
+  # TODO(robinson): Remove duplication with similar method
+  # for non-repeated scalars.
+  proto_field_name = field.name
+  python_field_name = _ValueFieldName(proto_field_name)
+  has_field_name = _HasFieldName(proto_field_name)
+  property_name = _PropertyName(proto_field_name)
+  message_type = field.message_type
+
+  def getter(self):
+    # TODO(robinson): Appropriately scary note about double-checked locking.
+    field_value = getattr(self, python_field_name)
+    if field_value is None:
+      self._lock.acquire()
+      try:
+        field_value = getattr(self, python_field_name)
+        if field_value is None:
+          field_class = message_type._concrete_class
+          field_value = field_class()
+          field_value._SetListener(_Listener(self, has_field_name))
+          setattr(self, python_field_name, field_value)
+      finally:
+        self._lock.release()
+    return field_value
+  getter.__module__ = None
+  getter.__doc__ = 'Getter for %s.' % proto_field_name
+
+  # We define a setter just so we can throw an exception with a more
+  # helpful error message.
+  def setter(self, new_value):
+    raise AttributeError('Assignment not allowed to composite field '
+                         '"%s" in protocol message object.' % proto_field_name)
+
+  # Add a property to encapsulate the getter.
+  doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name
+  setattr(cls, property_name, property(getter, setter, doc=doc))
+
+
+def _AddStaticMethods(cls):
+  # TODO(robinson): This probably needs to be thread-safe(?)
+  def RegisterExtension(extension_handle):
+    extension_handle.containing_type = cls.DESCRIPTOR
+    cls._known_extensions.append(extension_handle)
+  cls.RegisterExtension = staticmethod(RegisterExtension)
+
+
+def _AddListFieldsMethod(message_descriptor, cls):
+  """Helper for _AddMessageMethods()."""
+
+  # Ensure that we always list in ascending field-number order.
+  # For non-extension fields, we can do the sort once, here, at import-time.
+  # For extensions, we sort on each ListFields() call, though
+  # we could do better if we have to.
+  fields = sorted(message_descriptor.fields, key=lambda f: f.number)
+  has_field_names = (_HasFieldName(f.name) for f in fields)
+  value_field_names = (_ValueFieldName(f.name) for f in fields)
+  triplets = zip(has_field_names, value_field_names, fields)
+
+  def ListFields(self):
+    # We need to list all extension and non-extension fields
+    # together, in sorted order by field number.
+
+    # Step 0: Get an iterator over all "set" non-extension fields,
+    # sorted by field number.
+    # This iterator yields (field_number, field_descriptor, value) tuples.
+    def SortedSetFieldsIter():
+      # Note that triplets is already sorted by field number.
+      for has_field_name, value_field_name, field_descriptor in triplets:
+        if field_descriptor.label == _FieldDescriptor.LABEL_REPEATED:
+          value = getattr(self, _ValueFieldName(field_descriptor.name))
+          if len(value) > 0:
+            yield (field_descriptor.number, field_descriptor, value)
+        elif getattr(self, _HasFieldName(field_descriptor.name)):
+          value = getattr(self, _ValueFieldName(field_descriptor.name))
+          yield (field_descriptor.number, field_descriptor, value)
+    sorted_fields = SortedSetFieldsIter()
+
+    # Step 1: Get an iterator over all "set" extension fields,
+    # sorted by field number.
+    # This iterator ALSO yields (field_number, field_descriptor, value) tuples.
+    # TODO(robinson): It's not necessary to repeat this with each
+    # serialization call.  We can do better.
+    sorted_extension_fields = sorted(
+        [(f.number, f, v) for f, v in self.Extensions._ListSetExtensions()])
+
+    # Step 2: Create a composite iterator that merges the extension-
+    # and non-extension fields, and that still yields fields in
+    # sorted order.
+    all_set_fields = _ImergeSorted(sorted_fields, sorted_extension_fields)
+
+    # Step 3: Strip off the field numbers and return.
+    return [field[1:] for field in all_set_fields]
+
+  cls.ListFields = ListFields
+
+def _AddHasFieldMethod(cls):
+  """Helper for _AddMessageMethods()."""
+  def HasField(self, field_name):
+    try:
+      return getattr(self, _HasFieldName(field_name))
+    except AttributeError:
+      raise ValueError('Protocol message has no "%s" field.' % field_name)
+  cls.HasField = HasField
+
+
+def _AddClearFieldMethod(cls):
+  """Helper for _AddMessageMethods()."""
+  def ClearField(self, field_name):
+    try:
+      field = self.DESCRIPTOR.fields_by_name[field_name]
+    except KeyError:
+      raise ValueError('Protocol message has no "%s" field.' % field_name)
+    proto_field_name = field.name
+    python_field_name = _ValueFieldName(proto_field_name)
+    has_field_name = _HasFieldName(proto_field_name)
+    default_value = _DefaultValueForField(self, field)
+    if field.label == _FieldDescriptor.LABEL_REPEATED:
+      self._MarkByteSizeDirty()
+    else:
+      if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+        old_field_value = getattr(self, python_field_name)
+        if old_field_value is not None:
+          # Snip the old object out of the object tree.
+          old_field_value._SetListener(None)
+      if getattr(self, has_field_name):
+        setattr(self, has_field_name, False)
+        # Set dirty bit on ourself and parents only if
+        # we're actually changing state.
+        self._MarkByteSizeDirty()
+    setattr(self, python_field_name, default_value)
+  cls.ClearField = ClearField
+
+
+def _AddClearExtensionMethod(cls):
+  """Helper for _AddMessageMethods()."""
+  def ClearExtension(self, extension_handle):
+    self.Extensions._ClearExtension(extension_handle)
+  cls.ClearExtension = ClearExtension
+
+
+def _AddClearMethod(cls):
+  """Helper for _AddMessageMethods()."""
+  def Clear(self):
+    # Clear fields.
+    fields = self.DESCRIPTOR.fields
+    for field in fields:
+      self.ClearField(field.name)
+    # Clear extensions.
+    extensions = self.Extensions._ListSetExtensions()
+    for extension in extensions:
+      self.ClearExtension(extension[0])
+  cls.Clear = Clear
+
+
+def _AddHasExtensionMethod(cls):
+  """Helper for _AddMessageMethods()."""
+  def HasExtension(self, extension_handle):
+    return self.Extensions._HasExtension(extension_handle)
+  cls.HasExtension = HasExtension
+
+
+def _AddEqualsMethod(message_descriptor, cls):
+  """Helper for _AddMessageMethods()."""
+  def __eq__(self, other):
+    if self is other:
+      return True
+
+    # Compare all fields contained directly in this message.
+    for field_descriptor in message_descriptor.fields:
+      label = field_descriptor.label
+      property_name = _PropertyName(field_descriptor.name)
+      # Non-repeated field equality requires matching "has" bits as well
+      # as having an equal value.
+      if label != _FieldDescriptor.LABEL_REPEATED:
+        self_has = self.HasField(property_name)
+        other_has = other.HasField(property_name)
+        if self_has != other_has:
+          return False
+        if not self_has:
+          # If the "has" bit for this field is False, we must stop here.
+          # Otherwise we will recurse forever on recursively-defined protos.
+          continue
+      if getattr(self, property_name) != getattr(other, property_name):
+        return False
+
+    # Compare the extensions present in both messages.
+    return self.Extensions == other.Extensions
+  cls.__eq__ = __eq__
+
+
+def _AddSetListenerMethod(cls):
+  """Helper for _AddMessageMethods()."""
+  def SetListener(self, listener):
+    if listener is None:
+      self._listener = message_listener_mod.NullMessageListener()
+    else:
+      self._listener = listener
+  cls._SetListener = SetListener
+
+
+def _BytesForNonRepeatedElement(value, field_number, field_type):
+  """Returns the number of bytes needed to serialize a non-repeated element.
+  The returned byte count includes space for tag information and any
+  other additional space associated with serializing value.
+
+  Args:
+    value: Value we're serializing.
+    field_number: Field number of this value.  (Since the field number
+      is stored as part of a varint-encoded tag, this has an impact
+      on the total bytes required to serialize the value).
+    field_type: The type of the field.  One of the TYPE_* constants
+      within FieldDescriptor.
+  """
+  try:
+    fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type]
+    return fn(field_number, value)
+  except KeyError:
+    raise message_mod.EncodeError('Unrecognized field type: %d' % field_type)
+
+
+def _AddByteSizeMethod(message_descriptor, cls):
+  """Helper for _AddMessageMethods()."""
+
+  def BytesForField(message, field, value):
+    """Returns the number of bytes required to serialize a single field
+    in message.  The field may be repeated or not, composite or not.
+
+    Args:
+      message: The Message instance containing a field of the given type.
+      field: A FieldDescriptor describing the field of interest.
+      value: The value whose byte size we're interested in.
+
+    Returns: The number of bytes required to serialize the current value
+      of "field" in "message", including space for tags and any other
+      necessary information.
+    """
+
+    if _MessageSetField(field):
+      return wire_format.MessageSetItemByteSize(field.number, value)
+
+    field_number, field_type = field.number, field.type
+
+    # Repeated fields.
+    if field.label == _FieldDescriptor.LABEL_REPEATED:
+      elements = value
+    else:
+      elements = [value]
+
+    size = sum(_BytesForNonRepeatedElement(element, field_number, field_type)
+               for element in elements)
+    return size
+
+  fields = message_descriptor.fields
+  has_field_names = (_HasFieldName(f.name) for f in fields)
+  zipped = zip(has_field_names, fields)
+
+  def ByteSize(self):
+    if not self._cached_byte_size_dirty:
+      return self._cached_byte_size
+
+    size = 0
+    # Hardcoded fields first.
+    for has_field_name, field in zipped:
+      if (field.label == _FieldDescriptor.LABEL_REPEATED
+          or getattr(self, has_field_name)):
+        value = getattr(self, _ValueFieldName(field.name))
+        size += BytesForField(self, field, value)
+    # Extensions next.
+    for field, value in self.Extensions._ListSetExtensions():
+      size += BytesForField(self, field, value)
+
+    self._cached_byte_size = size
+    self._cached_byte_size_dirty = False
+    return size
+  cls.ByteSize = ByteSize
+
+
+def _MessageSetField(field_descriptor):
+  """Checks if a field should be serialized using the message set wire format.
+
+  Args:
+    field_descriptor: Descriptor of the field.
+
+  Returns:
+    True if the field should be serialized using the message set wire format,
+    false otherwise.
+  """
+  return (field_descriptor.is_extension and
+          field_descriptor.label != _FieldDescriptor.LABEL_REPEATED and
+          field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and
+          field_descriptor.containing_type.GetOptions().message_set_wire_format)
+
+
+def _SerializeValueToEncoder(value, field_number, field_descriptor, encoder):
+  """Appends the serialization of a single value to encoder.
+
+  Args:
+    value: Value to serialize.
+    field_number: Field number of this value.
+    field_descriptor: Descriptor of the field to serialize.
+    encoder: encoder.Encoder object to which we should serialize this value.
+  """
+  if _MessageSetField(field_descriptor):
+    encoder.AppendMessageSetItem(field_number, value)
+    return
+
+  try:
+    method = type_checkers.TYPE_TO_SERIALIZE_METHOD[field_descriptor.type]
+    method(encoder, field_number, value)
+  except KeyError:
+    raise message_mod.EncodeError('Unrecognized field type: %d' %
+                                  field_descriptor.type)
+
+
+def _ImergeSorted(*streams):
+  """Merges N sorted iterators into a single sorted iterator.
+  Each element in streams must be an iterable that yields
+  its elements in sorted order, and the elements contained
+  in each stream must all be comparable.
+
+  There may be repeated elements in the component streams or
+  across the streams; the repeated elements will all be repeated
+  in the merged iterator as well.
+
+  I believe that the heapq module at HEAD in the Python
+  sources has a method like this, but for now we roll our own.
+  """
+  iters = [iter(stream) for stream in streams]
+  heap = []
+  for index, it in enumerate(iters):
+    try:
+      heap.append((it.next(), index))
+    except StopIteration:
+      pass
+  heapq.heapify(heap)
+
+  while heap:
+    smallest_value, idx = heap[0]
+    yield smallest_value
+    try:
+      next_element = iters[idx].next()
+      heapq.heapreplace(heap, (next_element, idx))
+    except StopIteration:
+      heapq.heappop(heap)
+
+
+def _AddSerializeToStringMethod(message_descriptor, cls):
+  """Helper for _AddMessageMethods()."""
+
+  def SerializeToString(self):
+    # Check if the message has all of its required fields set.
+    errors = []
+    if not _InternalIsInitialized(self, errors):
+      raise message_mod.EncodeError('\n'.join(errors))
+    return self.SerializePartialToString()
+  cls.SerializeToString = SerializeToString
+
+
+def _AddSerializePartialToStringMethod(message_descriptor, cls):
+  """Helper for _AddMessageMethods()."""
+  Encoder = encoder.Encoder
+
+  def SerializePartialToString(self):
+    encoder = Encoder()
+    # We need to serialize all extension and non-extension fields
+    # together, in sorted order by field number.
+    for field_descriptor, field_value in self.ListFields():
+      if field_descriptor.label == _FieldDescriptor.LABEL_REPEATED:
+        repeated_value = field_value
+      else:
+        repeated_value = [field_value]
+      for element in repeated_value:
+        _SerializeValueToEncoder(element, field_descriptor.number,
+                                 field_descriptor, encoder)
+    return encoder.ToString()
+  cls.SerializePartialToString = SerializePartialToString
+
+
+def _WireTypeForFieldType(field_type):
+  """Given a field type, returns the expected wire type."""
+  try:
+    return type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_type]
+  except KeyError:
+    raise message_mod.DecodeError('Unknown field type: %d' % field_type)
+
+
+def _RecursivelyMerge(field_number, field_type, decoder, message):
+  """Decodes a message from decoder into message.
+  message is either a group or a nested message within some containing
+  protocol message.  If it's a group, we use the group protocol to
+  deserialize, and if it's a nested message, we use the nested-message
+  protocol.
+
+  Args:
+    field_number: The field number of message in its enclosing protocol buffer.
+    field_type: The field type of message.  Must be either TYPE_MESSAGE
+      or TYPE_GROUP.
+    decoder: Decoder to read from.
+    message: Message to deserialize into.
+  """
+  if field_type == _FieldDescriptor.TYPE_MESSAGE:
+    decoder.ReadMessageInto(message)
+  elif field_type == _FieldDescriptor.TYPE_GROUP:
+    decoder.ReadGroupInto(field_number, message)
+  else:
+    raise message_mod.DecodeError('Unexpected field type: %d' % field_type)
+
+
+def _DeserializeScalarFromDecoder(field_type, decoder):
+  """Deserializes a scalar of the requested type from decoder.  field_type must
+  be a scalar (non-group, non-message) FieldDescriptor.FIELD_* constant.
+  """
+  try:
+    method = type_checkers.TYPE_TO_DESERIALIZE_METHOD[field_type]
+    return method(decoder)
+  except KeyError:
+    raise message_mod.DecodeError('Unrecognized field type: %d' % field_type)
+
+
+def _SkipField(field_number, wire_type, decoder):
+  """Skips a field with the specified wire type.
+
+  Args:
+    field_number: Tag number of the field to skip.
+    wire_type: Wire type of the field to skip.
+    decoder: Decoder used to deserialize the messsage. It must be positioned
+      just after reading the the tag and wire type of the field.
+  """
+  if wire_type == wire_format.WIRETYPE_VARINT:
+    decoder.ReadUInt64()
+  elif wire_type == wire_format.WIRETYPE_FIXED64:
+    decoder.ReadFixed64()
+  elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED:
+    decoder.SkipBytes(decoder.ReadInt32())
+  elif wire_type == wire_format.WIRETYPE_START_GROUP:
+    _SkipGroup(field_number, decoder)
+  elif wire_type == wire_format.WIRETYPE_END_GROUP:
+    pass
+  elif wire_type == wire_format.WIRETYPE_FIXED32:
+    decoder.ReadFixed32()
+  else:
+    raise message_mod.DecodeError('Unexpected wire type: %d' % wire_type)
+
+
+def _SkipGroup(group_number, decoder):
+  """Skips a nested group from the decoder.
+
+  Args:
+    group_number: Tag number of the group to skip.
+    decoder: Decoder used to deserialize the message. It must be positioned
+      exactly at the beginning of the message that should be skipped.
+  """
+  while True:
+    field_number, wire_type = decoder.ReadFieldNumberAndWireType()
+    if (wire_type == wire_format.WIRETYPE_END_GROUP and
+        field_number == group_number):
+      return
+    _SkipField(field_number, wire_type, decoder)
+
+
+def _DeserializeMessageSetItem(message, decoder):
+  """Deserializes a message using the message set wire format.
+
+  Args:
+    message: Message to be parsed to.
+    decoder: The decoder to be used to deserialize encoded data. Note that the
+      decoder should be positioned just after reading the START_GROUP tag that
+      began the messageset item.
+  """
+  field_number, wire_type = decoder.ReadFieldNumberAndWireType()
+  if wire_type != wire_format.WIRETYPE_VARINT or field_number != 2:
+    raise message_mod.DecodeError(
+        'Incorrect message set wire format. '
+        'wire_type: %d, field_number: %d' % (wire_type, field_number))
+
+  type_id = decoder.ReadInt32()
+  field_number, wire_type = decoder.ReadFieldNumberAndWireType()
+  if wire_type != wire_format.WIRETYPE_LENGTH_DELIMITED or field_number != 3:
+    raise message_mod.DecodeError(
+        'Incorrect message set wire format. '
+        'wire_type: %d, field_number: %d' % (wire_type, field_number))
+
+  extension_dict = message.Extensions
+  extensions_by_number = extension_dict._AllExtensionsByNumber()
+  if type_id not in extensions_by_number:
+    _SkipField(field_number, wire_type, decoder)
+    return
+
+  field_descriptor = extensions_by_number[type_id]
+  value = extension_dict[field_descriptor]
+  decoder.ReadMessageInto(value)
+  # Read the END_GROUP tag.
+  field_number, wire_type = decoder.ReadFieldNumberAndWireType()
+  if wire_type != wire_format.WIRETYPE_END_GROUP or field_number != 1:
+    raise message_mod.DecodeError(
+        'Incorrect message set wire format. '
+        'wire_type: %d, field_number: %d' % (wire_type, field_number))
+
+
+def _DeserializeOneEntity(message_descriptor, message, decoder):
+  """Deserializes the next wire entity from decoder into message.
+  The next wire entity is either a scalar or a nested message,
+  and may also be an element in a repeated field (the wire encoding
+  is the same).
+
+  Args:
+    message_descriptor: A Descriptor instance describing all fields
+      in message.
+    message: The Message instance into which we're decoding our fields.
+    decoder: The Decoder we're using to deserialize encoded data.
+
+  Returns: The number of bytes read from decoder during this method.
+  """
+  initial_position = decoder.Position()
+  field_number, wire_type = decoder.ReadFieldNumberAndWireType()
+  extension_dict = message.Extensions
+  extensions_by_number = extension_dict._AllExtensionsByNumber()
+  if field_number in message_descriptor.fields_by_number:
+    # Non-extension field.
+    field_descriptor = message_descriptor.fields_by_number[field_number]
+    value = getattr(message, _PropertyName(field_descriptor.name))
+    def nonextension_setter_fn(scalar):
+      setattr(message, _PropertyName(field_descriptor.name), scalar)
+    scalar_setter_fn = nonextension_setter_fn
+  elif field_number in extensions_by_number:
+    # Extension field.
+    field_descriptor = extensions_by_number[field_number]
+    value = extension_dict[field_descriptor]
+    def extension_setter_fn(scalar):
+      extension_dict[field_descriptor] = scalar
+    scalar_setter_fn = extension_setter_fn
+  elif wire_type == wire_format.WIRETYPE_END_GROUP:
+    # We assume we're being parsed as the group that's ended.
+    return 0
+  elif (wire_type == wire_format.WIRETYPE_START_GROUP and
+        field_number == 1 and
+        message_descriptor.GetOptions().message_set_wire_format):
+    # A Message Set item.
+    _DeserializeMessageSetItem(message, decoder)
+    return decoder.Position() - initial_position
+  else:
+    _SkipField(field_number, wire_type, decoder)
+    return decoder.Position() - initial_position
+
+  # If we reach this point, we've identified the field as either
+  # hardcoded or extension, and set |field_descriptor|, |scalar_setter_fn|,
+  # and |value| appropriately.  Now actually deserialize the thing.
+  #
+  # field_descriptor: Describes the field we're deserializing.
+  # value: The value currently stored in the field to deserialize.
+  #   Used only if the field is composite and/or repeated.
+  # scalar_setter_fn: A function F such that F(scalar) will
+  #   set a nonrepeated scalar value for this field.  Used only
+  #   if this field is a nonrepeated scalar.
+
+  field_number = field_descriptor.number
+  field_type = field_descriptor.type
+  expected_wire_type = _WireTypeForFieldType(field_type)
+  if wire_type != expected_wire_type:
+    # Need to fill in uninterpreted_bytes.  Work for the next CL.
+    raise RuntimeError('TODO(robinson): Wiretype mismatches not handled.')
+
+  property_name = _PropertyName(field_descriptor.name)
+  label = field_descriptor.label
+  cpp_type = field_descriptor.cpp_type
+
+  # Nonrepeated scalar.  Just set the field directly.
+  if (label != _FieldDescriptor.LABEL_REPEATED
+      and cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE):
+    scalar_setter_fn(_DeserializeScalarFromDecoder(field_type, decoder))
+    return decoder.Position() - initial_position
+
+  # Nonrepeated composite.  Recursively deserialize.
+  if label != _FieldDescriptor.LABEL_REPEATED:
+    composite = value
+    _RecursivelyMerge(field_number, field_type, decoder, composite)
+    return decoder.Position() - initial_position
+
+  # Now we know we're dealing with a repeated field of some kind.
+  element_list = value
+
+  if cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE:
+    # Repeated scalar.
+    element_list.append(_DeserializeScalarFromDecoder(field_type, decoder))
+    return decoder.Position() - initial_position
+  else:
+    # Repeated composite.
+    composite = element_list.add()
+    _RecursivelyMerge(field_number, field_type, decoder, composite)
+    return decoder.Position() - initial_position
+
+
+def _FieldOrExtensionValues(message, field_or_extension):
+  """Retrieves the list of values for the specified field or extension.
+
+  The target field or extension can be optional, required or repeated, but it
+  must have value(s) set. The assumption is that the target field or extension
+  is set (e.g. _HasFieldOrExtension holds true).
+
+  Args:
+    message: Message which contains the target field or extension.
+    field_or_extension: Field or extension for which the list of values is
+      required. Must be an instance of FieldDescriptor.
+
+  Returns:
+    A list of values for the specified field or extension. This list will only
+    contain a single element if the field is non-repeated.
+  """
+  if field_or_extension.is_extension:
+    value = message.Extensions[field_or_extension]
+  else:
+    value = getattr(message, _ValueFieldName(field_or_extension.name))
+  if field_or_extension.label != _FieldDescriptor.LABEL_REPEATED:
+    return [value]
+  else:
+    # In this case value is a list or repeated values.
+    return value
+
+
+def _HasFieldOrExtension(message, field_or_extension):
+  """Checks if a message has the specified field or extension set.
+
+  The field or extension specified can be optional, required or repeated. If
+  it is repeated, this function returns True. Otherwise it checks the has bit
+  of the field or extension.
+
+  Args:
+    message: Message which contains the target field or extension.
+    field_or_extension: Field or extension to check. This must be a
+      FieldDescriptor instance.
+
+  Returns:
+    True if the message has a value set for the specified field or extension,
+    or if the field or extension is repeated.
+  """
+  if field_or_extension.label == _FieldDescriptor.LABEL_REPEATED:
+    return True
+  if field_or_extension.is_extension:
+    return message.HasExtension(field_or_extension)
+  else:
+    return message.HasField(field_or_extension.name)
+
+
+def _IsFieldOrExtensionInitialized(message, field, errors=None):
+  """Checks if a message field or extension is initialized.
+
+  Args:
+    message: The message which contains the field or extension.
+    field: Field or extension to check. This must be a FieldDescriptor instance.
+    errors: Errors will be appended to it, if set to a meaningful value.
+
+  Returns:
+    True if the field/extension can be considered initialized.
+  """
+  # If the field is required and is not set, it isn't initialized.
+  if field.label == _FieldDescriptor.LABEL_REQUIRED:
+    if not _HasFieldOrExtension(message, field):
+      if errors is not None:
+        errors.append('Required field %s is not set.' % field.full_name)
+      return False
+
+  # If the field is optional and is not set, or if it
+  # isn't a submessage then the field is initialized.
+  if field.label == _FieldDescriptor.LABEL_OPTIONAL:
+    if not _HasFieldOrExtension(message, field):
+      return True
+  if field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE:
+    return True
+
+  # The field is set and is either a single or a repeated submessage.
+  messages = _FieldOrExtensionValues(message, field)
+  # If all submessages in this field are initialized, the field is
+  # considered initialized.
+  for message in messages:
+    if not _InternalIsInitialized(message, errors):
+      return False
+  return True
+
+
+def _InternalIsInitialized(message, errors=None):
+  """Checks if all required fields of a message are set.
+
+  Args:
+    message: The message to check.
+    errors: If set, initialization errors will be appended to it.
+
+  Returns:
+    True iff the specified message has all required fields set.
+  """
+  fields_and_extensions = []
+  fields_and_extensions.extend(message.DESCRIPTOR.fields)
+  fields_and_extensions.extend(
+      [extension[0] for extension in message.Extensions._ListSetExtensions()])
+  for field_or_extension in fields_and_extensions:
+    if not _IsFieldOrExtensionInitialized(message, field_or_extension, errors):
+      return False
+  return True
+
+
+def _AddMergeFromStringMethod(message_descriptor, cls):
+  """Helper for _AddMessageMethods()."""
+  Decoder = decoder.Decoder
+  def MergeFromString(self, serialized):
+    decoder = Decoder(serialized)
+    byte_count = 0
+    while not decoder.EndOfStream():
+      bytes_read = _DeserializeOneEntity(message_descriptor, self, decoder)
+      if not bytes_read:
+        break
+      byte_count += bytes_read
+    return byte_count
+  cls.MergeFromString = MergeFromString
+
+
+def _AddIsInitializedMethod(cls):
+  """Adds the IsInitialized method to the protocol message class."""
+  cls.IsInitialized = _InternalIsInitialized
+
+
+def _MergeFieldOrExtension(destination_msg, field, value):
+  """Merges a specified message field into another message."""
+  property_name = _PropertyName(field.name)
+  is_extension = field.is_extension
+
+  if not is_extension:
+    destination = getattr(destination_msg, property_name)
+  elif (field.label == _FieldDescriptor.LABEL_REPEATED or
+        field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE):
+    destination = destination_msg.Extensions[field]
+
+  # Case 1 - a composite field.
+  if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+    if field.label == _FieldDescriptor.LABEL_REPEATED:
+      for v in value:
+        destination.add().MergeFrom(v)
+    else:
+      destination.MergeFrom(value)
+    return
+
+  # Case 2 - a repeated field.
+  if field.label == _FieldDescriptor.LABEL_REPEATED:
+    for v in value:
+      destination.append(v)
+    return
+
+  # Case 3 - a singular field.
+  if is_extension:
+    destination_msg.Extensions[field] = value
+  else:
+    setattr(destination_msg, property_name, value)
+
+
+def _AddMergeFromMethod(cls):
+  def MergeFrom(self, msg):
+    assert msg is not self
+    for field in msg.ListFields():
+      _MergeFieldOrExtension(self, field[0], field[1])
+  cls.MergeFrom = MergeFrom
+
+
+def _AddMessageMethods(message_descriptor, cls):
+  """Adds implementations of all Message methods to cls."""
+  _AddListFieldsMethod(message_descriptor, cls)
+  _AddHasFieldMethod(cls)
+  _AddClearFieldMethod(cls)
+  _AddClearExtensionMethod(cls)
+  _AddClearMethod(cls)
+  _AddHasExtensionMethod(cls)
+  _AddEqualsMethod(message_descriptor, cls)
+  _AddSetListenerMethod(cls)
+  _AddByteSizeMethod(message_descriptor, cls)
+  _AddSerializeToStringMethod(message_descriptor, cls)
+  _AddSerializePartialToStringMethod(message_descriptor, cls)
+  _AddMergeFromStringMethod(message_descriptor, cls)
+  _AddIsInitializedMethod(cls)
+  _AddMergeFromMethod(cls)
+
+
+def _AddPrivateHelperMethods(cls):
+  """Adds implementation of private helper methods to cls."""
+
+  def MaybeCallTransitionToNonemptyCallback(self):
+    """Calls self._listener.TransitionToNonempty() the first time this
+    method is called.  On all subsequent calls, this is a no-op.
+    """
+    if not self._called_transition_to_nonempty:
+      self._listener.TransitionToNonempty()
+      self._called_transition_to_nonempty = True
+  cls._MaybeCallTransitionToNonemptyCallback = (
+      MaybeCallTransitionToNonemptyCallback)
+
+  def MarkByteSizeDirty(self):
+    """Sets the _cached_byte_size_dirty bit to true,
+    and propagates this to our listener iff this was a state change.
+    """
+    if not self._cached_byte_size_dirty:
+      self._cached_byte_size_dirty = True
+      self._listener.ByteSizeDirty()
+  cls._MarkByteSizeDirty = MarkByteSizeDirty
+
+
+class _Listener(object):
+
+  """MessageListener implementation that a parent message registers with its
+  child message.
+
+  In order to support semantics like:
+
+    foo.bar.baz = 23
+    assert foo.HasField('bar')
+
+  ...child objects must have back references to their parents.
+  This helper class is at the heart of this support.
+  """
+
+  def __init__(self, parent_message, has_field_name):
+    """Args:
+      parent_message: The message whose _MaybeCallTransitionToNonemptyCallback()
+        and _MarkByteSizeDirty() methods we should call when we receive
+        TransitionToNonempty() and ByteSizeDirty() messages.
+      has_field_name: The name of the "has" field that we should set in
+        the parent message when we receive a TransitionToNonempty message,
+        or None if there's no "has" field to set.  (This will be the case
+        for child objects in "repeated" fields).
+    """
+    # This listener establishes a back reference from a child (contained) object
+    # to its parent (containing) object.  We make this a weak reference to avoid
+    # creating cyclic garbage when the client finishes with the 'parent' object
+    # in the tree.
+    if isinstance(parent_message, weakref.ProxyType):
+      self._parent_message_weakref = parent_message
+    else:
+      self._parent_message_weakref = weakref.proxy(parent_message)
+    self._has_field_name = has_field_name
+
+  def TransitionToNonempty(self):
+    try:
+      if self._has_field_name is not None:
+        setattr(self._parent_message_weakref, self._has_field_name, True)
+      # Propagate the signal to our parents iff this is the first field set.
+      self._parent_message_weakref._MaybeCallTransitionToNonemptyCallback()
+    except ReferenceError:
+      # We can get here if a client has kept a reference to a child object,
+      # and is now setting a field on it, but the child's parent has been
+      # garbage-collected.  This is not an error.
+      pass
+
+  def ByteSizeDirty(self):
+    try:
+      self._parent_message_weakref._MarkByteSizeDirty()
+    except ReferenceError:
+      # Same as above.
+      pass
+
+
+# TODO(robinson): Move elsewhere?
+# TODO(robinson): Provide a clear() method here in addition to ClearField()?
+class _RepeatedScalarFieldContainer(object):
+
+  """Simple, type-checked, list-like container for holding repeated scalars."""
+
+  # Minimizes memory usage and disallows assignment to other attributes.
+  __slots__ = ['_message_listener', '_type_checker', '_values']
+
+  def __init__(self, message_listener, type_checker):
+    """
+    Args:
+      message_listener: A MessageListener implementation.
+        The _RepeatedScalarFieldContaininer will call this object's
+        TransitionToNonempty() method when it transitions from being empty to
+        being nonempty.
+      type_checker: A _ValueChecker instance to run on elements inserted
+        into this container.
+    """
+    self._message_listener = message_listener
+    self._type_checker = type_checker
+    self._values = []
+
+  def append(self, elem):
+    self._type_checker.CheckValue(elem)
+    self._values.append(elem)
+    self._message_listener.ByteSizeDirty()
+    if len(self._values) == 1:
+      self._message_listener.TransitionToNonempty()
+
+  def remove(self, elem):
+    self._values.remove(elem)
+    self._message_listener.ByteSizeDirty()
+
+  # List-like __getitem__() support also makes us iterable (via "iter(foo)"
+  # or implicitly via "for i in mylist:") for free.
+  def __getitem__(self, key):
+    return self._values[key]
+
+  def __setitem__(self, key, value):
+    # No need to call TransitionToNonempty(), since if we're able to
+    # set the element at this index, we were already nonempty before
+    # this method was called.
+    self._message_listener.ByteSizeDirty()
+    self._type_checker.CheckValue(value)
+    self._values[key] = value
+
+  def __len__(self):
+    return len(self._values)
+
+  def __eq__(self, other):
+    if self is other:
+      return True
+    # Special case for the same type which should be common and fast.
+    if isinstance(other, self.__class__):
+      return other._values == self._values
+    # We are presumably comparing against some other sequence type.
+    return other == self._values
+
+  def __ne__(self, other):
+    # Can't use != here since it would infinitely recurse.
+    return not self == other
+
+
+# TODO(robinson): Move elsewhere?
+# TODO(robinson): Provide a clear() method here in addition to ClearField()?
+# TODO(robinson): Unify common functionality with
+# _RepeatedScalarFieldContaininer?
+class _RepeatedCompositeFieldContainer(object):
+
+  """Simple, list-like container for holding repeated composite fields."""
+
+  # Minimizes memory usage and disallows assignment to other attributes.
+  __slots__ = ['_values', '_message_descriptor', '_message_listener']
+
+  def __init__(self, message_listener, message_descriptor):
+    """Note that we pass in a descriptor instead of the generated directly,
+    since at the time we construct a _RepeatedCompositeFieldContainer we
+    haven't yet necessarily initialized the type that will be contained in the
+    container.
+
+    Args:
+      message_listener: A MessageListener implementation.
+        The _RepeatedCompositeFieldContainer will call this object's
+        TransitionToNonempty() method when it transitions from being empty to
+        being nonempty.
+      message_descriptor: A Descriptor instance describing the protocol type
+        that should be present in this container.  We'll use the
+        _concrete_class field of this descriptor when the client calls add().
+    """
+    self._message_listener = message_listener
+    self._message_descriptor = message_descriptor
+    self._values = []
+
+  def add(self):
+    new_element = self._message_descriptor._concrete_class()
+    new_element._SetListener(self._message_listener)
+    self._values.append(new_element)
+    self._message_listener.ByteSizeDirty()
+    self._message_listener.TransitionToNonempty()
+    return new_element
+
+  def __delitem__(self, key):
+    self._message_listener.ByteSizeDirty()
+    del self._values[key]
+
+  # List-like __getitem__() support also makes us iterable (via "iter(foo)"
+  # or implicitly via "for i in mylist:") for free.
+  def __getitem__(self, key):
+    return self._values[key]
+
+  def __len__(self):
+    return len(self._values)
+
+  def __eq__(self, other):
+    if self is other:
+      return True
+    if not isinstance(other, self.__class__):
+      raise TypeError('Can only compare repeated composite fields against '
+                      'other repeated composite fields.')
+    return self._values == other._values
+
+  def __ne__(self, other):
+    # Can't use != here since it would infinitely recurse.
+    return not self == other
+
+  # TODO(robinson): Implement, document, and test slicing support.
+
+
+# TODO(robinson): Move elsewhere?  This file is getting pretty ridiculous...
+# TODO(robinson): Unify error handling of "unknown extension" crap.
+# TODO(robinson): There's so much similarity between the way that
+# extensions behave and the way that normal fields behave that it would
+# be really nice to unify more code.  It's not immediately obvious
+# how to do this, though, and I'd rather get the full functionality
+# implemented (and, crucially, get all the tests and specs fleshed out
+# and passing), and then come back to this thorny unification problem.
+# TODO(robinson): Support iteritems()-style iteration over all
+# extensions with the "has" bits turned on?
+class _ExtensionDict(object):
+
+  """Dict-like container for supporting an indexable "Extensions"
+  field on proto instances.
+
+  Note that in all cases we expect extension handles to be
+  FieldDescriptors.
+  """
+
+  class _ExtensionListener(object):
+
+    """Adapts an _ExtensionDict to behave as a MessageListener."""
+
+    def __init__(self, extension_dict, handle_id):
+      self._extension_dict = extension_dict
+      self._handle_id = handle_id
+
+    def TransitionToNonempty(self):
+      self._extension_dict._SubmessageTransitionedToNonempty(self._handle_id)
+
+    def ByteSizeDirty(self):
+      self._extension_dict._SubmessageByteSizeBecameDirty()
+
+  # TODO(robinson): Somewhere, we need to blow up if people
+  # try to register two extensions with the same field number.
+  # (And we need a test for this of course).
+
+  def __init__(self, extended_message, known_extensions):
+    """extended_message: Message instance for which we are the Extensions dict.
+      known_extensions: Iterable of known extension handles.
+        These must be FieldDescriptors.
+    """
+    # We keep a weak reference to extended_message, since
+    # it has a reference to this instance in turn.
+    self._extended_message = weakref.proxy(extended_message)
+    # We make a deep copy of known_extensions to avoid any
+    # thread-safety concerns, since the argument passed in
+    # is the global (class-level) dict of known extensions for
+    # this type of message, which could be modified at any time
+    # via a RegisterExtension() call.
+    #
+    # This dict maps from handle id to handle (a FieldDescriptor).
+    #
+    # XXX
+    # TODO(robinson): This isn't good enough.  The client could
+    # instantiate an object in module A, then afterward import
+    # module B and pass the instance to B.Foo().  If B imports
+    # an extender of this proto and then tries to use it, B
+    # will get a KeyError, even though the extension *is* registered
+    # at the time of use.
+    # XXX
+    self._known_extensions = dict((id(e), e) for e in known_extensions)
+    # Read lock around self._values, which may be modified by multiple
+    # concurrent readers in the conceptually "const" __getitem__ method.
+    # So, we grab this lock in every "read-only" method to ensure
+    # that concurrent read access is safe without external locking.
+    self._lock = threading.Lock()
+    # Maps from extension handle ID to current value of that extension.
+    self._values = {}
+    # Maps from extension handle ID to a boolean "has" bit, but only
+    # for non-repeated extension fields.
+    keys = (id for id, extension in self._known_extensions.iteritems()
+            if extension.label != _FieldDescriptor.LABEL_REPEATED)
+    self._has_bits = dict.fromkeys(keys, False)
+
+  def __getitem__(self, extension_handle):
+    """Returns the current value of the given extension handle."""
+    # We don't care as much about keeping critical sections short in the
+    # extension support, since it's presumably much less of a common case.
+    self._lock.acquire()
+    try:
+      handle_id = id(extension_handle)
+      if handle_id not in self._known_extensions:
+        raise KeyError('Extension not known to this class')
+      if handle_id not in self._values:
+        self._AddMissingHandle(extension_handle, handle_id)
+      return self._values[handle_id]
+    finally:
+      self._lock.release()
+
+  def __eq__(self, other):
+    # We have to grab read locks since we're accessing _values
+    # in a "const" method.  See the comment in the constructor.
+    if self is other:
+      return True
+    self._lock.acquire()
+    try:
+      other._lock.acquire()
+      try:
+        if self._has_bits != other._has_bits:
+          return False
+        # If there's a "has" bit, then only compare values where it is true.
+        for k, v in self._values.iteritems():
+          if self._has_bits.get(k, False) and v != other._values[k]:
+            return False
+        return True
+      finally:
+        other._lock.release()
+    finally:
+      self._lock.release()
+
+  def __ne__(self, other):
+    return not self == other
+
+  # Note that this is only meaningful for non-repeated, scalar extension
+  # fields.  Note also that we may have to call
+  # MaybeCallTransitionToNonemptyCallback() when we do successfully set a field
+  # this way, to set any necssary "has" bits in the ancestors of the extended
+  # message.
+  def __setitem__(self, extension_handle, value):
+    """If extension_handle specifies a non-repeated, scalar extension
+    field, sets the value of that field.
+    """
+    handle_id = id(extension_handle)
+    if handle_id not in self._known_extensions:
+      raise KeyError('Extension not known to this class')
+    field = extension_handle  # Just shorten the name.
+    if (field.label == _FieldDescriptor.LABEL_OPTIONAL
+        and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE):
+      # It's slightly wasteful to lookup the type checker each time,
+      # but we expect this to be a vanishingly uncommon case anyway.
+      type_checker = type_checkers.GetTypeChecker(field.cpp_type, field.type)
+      type_checker.CheckValue(value)
+      self._values[handle_id] = value
+      self._has_bits[handle_id] = True
+      self._extended_message._MarkByteSizeDirty()
+      self._extended_message._MaybeCallTransitionToNonemptyCallback()
+    else:
+      raise TypeError('Extension is repeated and/or a composite type.')
+
+  def _AddMissingHandle(self, extension_handle, handle_id):
+    """Helper internal to ExtensionDict."""
+    # Special handling for non-repeated message extensions, which (like
+    # normal fields of this kind) are initialized lazily.
+    # REQUIRES: _lock already held.
+    cpp_type = extension_handle.cpp_type
+    label = extension_handle.label
+    if (cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE
+        and label != _FieldDescriptor.LABEL_REPEATED):
+      self._AddMissingNonRepeatedCompositeHandle(extension_handle, handle_id)
+    else:
+      self._values[handle_id] = _DefaultValueForField(
+          self._extended_message, extension_handle)
+
+  def _AddMissingNonRepeatedCompositeHandle(self, extension_handle, handle_id):
+    """Helper internal to ExtensionDict."""
+    # REQUIRES: _lock already held.
+    value = extension_handle.message_type._concrete_class()
+    value._SetListener(_ExtensionDict._ExtensionListener(self, handle_id))
+    self._values[handle_id] = value
+
+  def _SubmessageTransitionedToNonempty(self, handle_id):
+    """Called when a submessage with a given handle id first transitions to
+    being nonempty.  Called by _ExtensionListener.
+    """
+    assert handle_id in self._has_bits
+    self._has_bits[handle_id] = True
+    self._extended_message._MaybeCallTransitionToNonemptyCallback()
+
+  def _SubmessageByteSizeBecameDirty(self):
+    """Called whenever a submessage's cached byte size becomes invalid
+    (goes from being "clean" to being "dirty").  Called by _ExtensionListener.
+    """
+    self._extended_message._MarkByteSizeDirty()
+
+  # We may wish to widen the public interface of Message.Extensions
+  # to expose some of this private functionality in the future.
+  # For now, we make all this functionality module-private and just
+  # implement what we need for serialization/deserialization,
+  # HasField()/ClearField(), etc.
+
+  def _HasExtension(self, extension_handle):
+    """Method for internal use by this module.
+    Returns true iff we "have" this extension in the sense of the
+    "has" bit being set.
+    """
+    handle_id = id(extension_handle)
+    # Note that this is different from the other checks.
+    if handle_id not in self._has_bits:
+      raise KeyError('Extension not known to this class, or is repeated field.')
+    return self._has_bits[handle_id]
+
+  # Intentionally pretty similar to ClearField() above.
+  def _ClearExtension(self, extension_handle):
+    """Method for internal use by this module.
+    Clears the specified extension, unsetting its "has" bit.
+    """
+    handle_id = id(extension_handle)
+    if handle_id not in self._known_extensions:
+      raise KeyError('Extension not known to this class')
+    default_value = _DefaultValueForField(self._extended_message,
+                                          extension_handle)
+    if extension_handle.label == _FieldDescriptor.LABEL_REPEATED:
+      self._extended_message._MarkByteSizeDirty()
+    else:
+      cpp_type = extension_handle.cpp_type
+      if cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+        if handle_id in self._values:
+          # Future modifications to this object shouldn't set any
+          # "has" bits here.
+          self._values[handle_id]._SetListener(None)
+      if self._has_bits[handle_id]:
+        self._has_bits[handle_id] = False
+        self._extended_message._MarkByteSizeDirty()
+    if handle_id in self._values:
+      del self._values[handle_id]
+
+  def _ListSetExtensions(self):
+    """Method for internal use by this module.
+
+    Returns an sequence of all extensions that are currently "set"
+    in this extension dict.  A "set" extension is a repeated extension,
+    or a non-repeated extension with its "has" bit set.
+
+    The returned sequence contains (field_descriptor, value) pairs,
+    where value is the current value of the extension with the given
+    field descriptor.
+
+    The sequence values are in arbitrary order.
+    """
+    self._lock.acquire()  # Read-only methods must lock around self._values.
+    try:
+      set_extensions = []
+      for handle_id, value in self._values.iteritems():
+        handle = self._known_extensions[handle_id]
+        if (handle.label == _FieldDescriptor.LABEL_REPEATED
+            or self._has_bits[handle_id]):
+          set_extensions.append((handle, value))
+      return set_extensions
+    finally:
+      self._lock.release()
+
+  def _AllExtensionsByNumber(self):
+    """Method for internal use by this module.
+
+    Returns: A dict mapping field_number to (handle, field_descriptor),
+      for *all* registered extensions for this dict.
+    """
+    # TODO(robinson): Precompute and store this away.  Note that we'll have to
+    # be careful when we move away from having _known_extensions as a
+    # deep-copied member of this object.
+    return dict((f.number, f) for f in self._known_extensions.itervalues())
diff --git a/froofle/protobuf/service.py b/froofle/protobuf/service.py
new file mode 100644
index 0000000..3989216
--- /dev/null
+++ b/froofle/protobuf/service.py
@@ -0,0 +1,208 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Declares the RPC service interfaces.
+
+This module declares the abstract interfaces underlying proto2 RPC
+services.  These are intented to be independent of any particular RPC
+implementation, so that proto2 services can be used on top of a variety
+of implementations.
+"""
+
+__author__ = 'petar@google.com (Petar Petrov)'
+
+
+class Service(object):
+
+  """Abstract base interface for protocol-buffer-based RPC services.
+
+  Services themselves are abstract classes (implemented either by servers or as
+  stubs), but they subclass this base interface. The methods of this
+  interface can be used to call the methods of the service without knowing
+  its exact type at compile time (analogous to the Message interface).
+  """
+
+  def GetDescriptor(self):
+    """Retrieves this service's descriptor."""
+    raise NotImplementedError
+
+  def CallMethod(self, method_descriptor, rpc_controller,
+                 request, done):
+    """Calls a method of the service specified by method_descriptor.
+
+    Preconditions:
+    * method_descriptor.service == GetDescriptor
+    * request is of the exact same classes as returned by
+      GetRequestClass(method).
+    * After the call has started, the request must not be modified.
+    * "rpc_controller" is of the correct type for the RPC implementation being
+      used by this Service.  For stubs, the "correct type" depends on the
+      RpcChannel which the stub is using.
+
+    Postconditions:
+    * "done" will be called when the method is complete.  This may be
+      before CallMethod() returns or it may be at some point in the future.
+    """
+    raise NotImplementedError
+
+  def GetRequestClass(self, method_descriptor):
+    """Returns the class of the request message for the specified method.
+
+    CallMethod() requires that the request is of a particular subclass of
+    Message. GetRequestClass() gets the default instance of this required
+    type.
+
+    Example:
+      method = service.GetDescriptor().FindMethodByName("Foo")
+      request = stub.GetRequestClass(method)()
+      request.ParseFromString(input)
+      service.CallMethod(method, request, callback)
+    """
+    raise NotImplementedError
+
+  def GetResponseClass(self, method_descriptor):
+    """Returns the class of the response message for the specified method.
+
+    This method isn't really needed, as the RpcChannel's CallMethod constructs
+    the response protocol message. It's provided anyway in case it is useful
+    for the caller to know the response type in advance.
+    """
+    raise NotImplementedError
+
+
+class RpcController(object):
+
+  """An RpcController mediates a single method call.
+
+  The primary purpose of the controller is to provide a way to manipulate
+  settings specific to the RPC implementation and to find out about RPC-level
+  errors. The methods provided by the RpcController interface are intended
+  to be a "least common denominator" set of features which we expect all
+  implementations to support.  Specific implementations may provide more
+  advanced features (e.g. deadline propagation).
+  """
+
+  # Client-side methods below
+
+  def Reset(self):
+    """Resets the RpcController to its initial state.
+
+    After the RpcController has been reset, it may be reused in
+    a new call. Must not be called while an RPC is in progress.
+    """
+    raise NotImplementedError
+
+  def Failed(self):
+    """Returns true if the call failed.
+
+    After a call has finished, returns true if the call failed.  The possible
+    reasons for failure depend on the RPC implementation.  Failed() must not
+    be called before a call has finished.  If Failed() returns true, the
+    contents of the response message are undefined.
+    """
+    raise NotImplementedError
+
+  def ErrorText(self):
+    """If Failed is true, returns a human-readable description of the error."""
+    raise NotImplementedError
+
+  def StartCancel(self):
+    """Initiate cancellation.
+
+    Advises the RPC system that the caller desires that the RPC call be
+    canceled.  The RPC system may cancel it immediately, may wait awhile and
+    then cancel it, or may not even cancel the call at all.  If the call is
+    canceled, the "done" callback will still be called and the RpcController
+    will indicate that the call failed at that time.
+    """
+    raise NotImplementedError
+
+  # Server-side methods below
+
+  def SetFailed(self, reason):
+    """Sets a failure reason.
+
+    Causes Failed() to return true on the client side.  "reason" will be
+    incorporated into the message returned by ErrorText().  If you find
+    you need to return machine-readable information about failures, you
+    should incorporate it into your response protocol buffer and should
+    NOT call SetFailed().
+    """
+    raise NotImplementedError
+
+  def IsCanceled(self):
+    """Checks if the client cancelled the RPC.
+
+    If true, indicates that the client canceled the RPC, so the server may
+    as well give up on replying to it.  The server should still call the
+    final "done" callback.
+    """
+    raise NotImplementedError
+
+  def NotifyOnCancel(self, callback):
+    """Sets a callback to invoke on cancel.
+
+    Asks that the given callback be called when the RPC is canceled.  The
+    callback will always be called exactly once.  If the RPC completes without
+    being canceled, the callback will be called after completion.  If the RPC
+    has already been canceled when NotifyOnCancel() is called, the callback
+    will be called immediately.
+
+    NotifyOnCancel() must be called no more than once per request.
+    """
+    raise NotImplementedError
+
+
+class RpcChannel(object):
+
+  """Abstract interface for an RPC channel.
+
+  An RpcChannel represents a communication line to a service which can be used
+  to call that service's methods.  The service may be running on another
+  machine. Normally, you should not use an RpcChannel directly, but instead
+  construct a stub {@link Service} wrapping it.  Example:
+
+  Example:
+    RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234")
+    RpcController controller = rpcImpl.Controller()
+    MyService service = MyService_Stub(channel)
+    service.MyMethod(controller, request, callback)
+  """
+
+  def CallMethod(self, method_descriptor, rpc_controller,
+                 request, response_class, done):
+    """Calls the method identified by the descriptor.
+
+    Call the given method of the remote service.  The signature of this
+    procedure looks the same as Service.CallMethod(), but the requirements
+    are less strict in one important way:  the request object doesn't have to
+    be of any specific class as long as its descriptor is method.input_type.
+    """
+    raise NotImplementedError
diff --git a/froofle/protobuf/service_reflection.py b/froofle/protobuf/service_reflection.py
new file mode 100644
index 0000000..bdd6bad
--- /dev/null
+++ b/froofle/protobuf/service_reflection.py
@@ -0,0 +1,289 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Contains metaclasses used to create protocol service and service stub
+classes from ServiceDescriptor objects at runtime.
+
+The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to
+inject all useful functionality into the classes output by the protocol
+compiler at compile-time.
+"""
+
+__author__ = 'petar@google.com (Petar Petrov)'
+
+
+class GeneratedServiceType(type):
+
+  """Metaclass for service classes created at runtime from ServiceDescriptors.
+
+  Implementations for all methods described in the Service class are added here
+  by this class. We also create properties to allow getting/setting all fields
+  in the protocol message.
+
+  The protocol compiler currently uses this metaclass to create protocol service
+  classes at runtime. Clients can also manually create their own classes at
+  runtime, as in this example:
+
+  mydescriptor = ServiceDescriptor(.....)
+  class MyProtoService(service.Service):
+    __metaclass__ = GeneratedServiceType
+    DESCRIPTOR = mydescriptor
+  myservice_instance = MyProtoService()
+  ...
+  """
+
+  _DESCRIPTOR_KEY = 'DESCRIPTOR'
+
+  def __init__(cls, name, bases, dictionary):
+    """Creates a message service class.
+
+    Args:
+      name: Name of the class (ignored, but required by the metaclass
+        protocol).
+      bases: Base classes of the class being constructed.
+      dictionary: The class dictionary of the class being constructed.
+        dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object
+        describing this protocol service type.
+    """
+    # Don't do anything if this class doesn't have a descriptor. This happens
+    # when a service class is subclassed.
+    if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary:
+      return
+    descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY]
+    service_builder = _ServiceBuilder(descriptor)
+    service_builder.BuildService(cls)
+
+
+class GeneratedServiceStubType(GeneratedServiceType):
+
+  """Metaclass for service stubs created at runtime from ServiceDescriptors.
+
+  This class has similar responsibilities as GeneratedServiceType, except that
+  it creates the service stub classes.
+  """
+
+  _DESCRIPTOR_KEY = 'DESCRIPTOR'
+
+  def __init__(cls, name, bases, dictionary):
+    """Creates a message service stub class.
+
+    Args:
+      name: Name of the class (ignored, here).
+      bases: Base classes of the class being constructed.
+      dictionary: The class dictionary of the class being constructed.
+        dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object
+        describing this protocol service type.
+    """
+    super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary)
+    # Don't do anything if this class doesn't have a descriptor. This happens
+    # when a service stub is subclassed.
+    if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary:
+      return
+    descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY]
+    service_stub_builder = _ServiceStubBuilder(descriptor)
+    service_stub_builder.BuildServiceStub(cls)
+
+
+class _ServiceBuilder(object):
+
+  """This class constructs a protocol service class using a service descriptor.
+
+  Given a service descriptor, this class constructs a class that represents
+  the specified service descriptor. One service builder instance constructs
+  exactly one service class. That means all instances of that class share the
+  same builder.
+  """
+
+  def __init__(self, service_descriptor):
+    """Initializes an instance of the service class builder.
+
+    Args:
+      service_descriptor: ServiceDescriptor to use when constructing the
+        service class.
+    """
+    self.descriptor = service_descriptor
+
+  def BuildService(self, cls):
+    """Constructs the service class.
+
+    Args:
+      cls: The class that will be constructed.
+    """
+
+    # CallMethod needs to operate with an instance of the Service class. This
+    # internal wrapper function exists only to be able to pass the service
+    # instance to the method that does the real CallMethod work.
+    def _WrapCallMethod(srvc, method_descriptor,
+                        rpc_controller, request, callback):
+      self._CallMethod(srvc, method_descriptor,
+                       rpc_controller, request, callback)
+    self.cls = cls
+    cls.CallMethod = _WrapCallMethod
+    cls.GetDescriptor = self._GetDescriptor
+    cls.GetRequestClass = self._GetRequestClass
+    cls.GetResponseClass = self._GetResponseClass
+    for method in self.descriptor.methods:
+      setattr(cls, method.name, self._GenerateNonImplementedMethod(method))
+
+  def _GetDescriptor(self):
+    """Retrieves the service descriptor.
+
+    Returns:
+      The descriptor of the service (of type ServiceDescriptor).
+    """
+    return self.descriptor
+
+  def _CallMethod(self, srvc, method_descriptor,
+                  rpc_controller, request, callback):
+    """Calls the method described by a given method descriptor.
+
+    Args:
+      srvc: Instance of the service for which this method is called.
+      method_descriptor: Descriptor that represent the method to call.
+      rpc_controller: RPC controller to use for this method's execution.
+      request: Request protocol message.
+      callback: A callback to invoke after the method has completed.
+    """
+    if method_descriptor.containing_service != self.descriptor:
+      raise RuntimeError(
+          'CallMethod() given method descriptor for wrong service type.')
+    method = getattr(srvc, method_descriptor.name)
+    method(rpc_controller, request, callback)
+
+  def _GetRequestClass(self, method_descriptor):
+    """Returns the class of the request protocol message.
+
+    Args:
+      method_descriptor: Descriptor of the method for which to return the
+        request protocol message class.
+
+    Returns:
+      A class that represents the input protocol message of the specified
+      method.
+    """
+    if method_descriptor.containing_service != self.descriptor:
+      raise RuntimeError(
+          'GetRequestClass() given method descriptor for wrong service type.')
+    return method_descriptor.input_type._concrete_class
+
+  def _GetResponseClass(self, method_descriptor):
+    """Returns the class of the response protocol message.
+
+    Args:
+      method_descriptor: Descriptor of the method for which to return the
+        response protocol message class.
+
+    Returns:
+      A class that represents the output protocol message of the specified
+      method.
+    """
+    if method_descriptor.containing_service != self.descriptor:
+      raise RuntimeError(
+          'GetResponseClass() given method descriptor for wrong service type.')
+    return method_descriptor.output_type._concrete_class
+
+  def _GenerateNonImplementedMethod(self, method):
+    """Generates and returns a method that can be set for a service methods.
+
+    Args:
+      method: Descriptor of the service method for which a method is to be
+        generated.
+
+    Returns:
+      A method that can be added to the service class.
+    """
+    return lambda inst, rpc_controller, request, callback: (
+        self._NonImplementedMethod(method.name, rpc_controller, callback))
+
+  def _NonImplementedMethod(self, method_name, rpc_controller, callback):
+    """The body of all methods in the generated service class.
+
+    Args:
+      method_name: Name of the method being executed.
+      rpc_controller: RPC controller used to execute this method.
+      callback: A callback which will be invoked when the method finishes.
+    """
+    rpc_controller.SetFailed('Method %s not implemented.' % method_name)
+    callback(None)
+
+
+class _ServiceStubBuilder(object):
+
+  """Constructs a protocol service stub class using a service descriptor.
+
+  Given a service descriptor, this class constructs a suitable stub class.
+  A stub is just a type-safe wrapper around an RpcChannel which emulates a
+  local implementation of the service.
+
+  One service stub builder instance constructs exactly one class. It means all
+  instances of that class share the same service stub builder.
+  """
+
+  def __init__(self, service_descriptor):
+    """Initializes an instance of the service stub class builder.
+
+    Args:
+      service_descriptor: ServiceDescriptor to use when constructing the
+        stub class.
+    """
+    self.descriptor = service_descriptor
+
+  def BuildServiceStub(self, cls):
+    """Constructs the stub class.
+
+    Args:
+      cls: The class that will be constructed.
+    """
+
+    def _ServiceStubInit(stub, rpc_channel):
+      stub.rpc_channel = rpc_channel
+    self.cls = cls
+    cls.__init__ = _ServiceStubInit
+    for method in self.descriptor.methods:
+      setattr(cls, method.name, self._GenerateStubMethod(method))
+
+  def _GenerateStubMethod(self, method):
+    return lambda inst, rpc_controller, request, callback: self._StubMethod(
+        inst, method, rpc_controller, request, callback)
+
+  def _StubMethod(self, stub, method_descriptor,
+                  rpc_controller, request, callback):
+    """The body of all service methods in the generated stub class.
+
+    Args:
+      stub: Stub instance.
+      method_descriptor: Descriptor of the invoked method.
+      rpc_controller: Rpc controller to execute the method.
+      request: Request protocol message.
+      callback: A callback to execute when the method finishes.
+    """
+    stub.rpc_channel.CallMethod(
+        method_descriptor, rpc_controller, request,
+        method_descriptor.output_type._concrete_class, callback)
diff --git a/froofle/protobuf/text_format.py b/froofle/protobuf/text_format.py
new file mode 100644
index 0000000..1c4cadf
--- /dev/null
+++ b/froofle/protobuf/text_format.py
@@ -0,0 +1,125 @@
+# Protocol Buffers - Google's data interchange format
+# Copyright 2008 Google Inc.  All rights reserved.
+# http://code.google.com/p/protobuf/
+#
+# 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.
+
+"""Contains routines for printing protocol messages in text format."""
+
+__author__ = 'kenton@google.com (Kenton Varda)'
+
+import cStringIO
+
+from froofle.protobuf import descriptor
+
+__all__ = [ 'MessageToString', 'PrintMessage', 'PrintField', 'PrintFieldValue' ]
+
+def MessageToString(message):
+  out = cStringIO.StringIO()
+  PrintMessage(message, out)
+  result = out.getvalue()
+  out.close()
+  return result
+
+def PrintMessage(message, out, indent = 0):
+  for field, value in message.ListFields():
+    if field.label == descriptor.FieldDescriptor.LABEL_REPEATED:
+      for element in value:
+        PrintField(field, element, out, indent)
+    else:
+      PrintField(field, value, out, indent)
+
+def PrintField(field, value, out, indent = 0):
+  """Print a single field name/value pair.  For repeated fields, the value
+  should be a single element."""
+
+  out.write(' ' * indent);
+  if field.is_extension:
+    out.write('[')
+    if (field.containing_type.GetOptions().message_set_wire_format and
+        field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and
+        field.message_type == field.extension_scope and
+        field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL):
+      out.write(field.message_type.full_name)
+    else:
+      out.write(field.full_name)
+    out.write(']')
+  elif field.type == descriptor.FieldDescriptor.TYPE_GROUP:
+    # For groups, use the capitalized name.
+    out.write(field.message_type.name)
+  else:
+    out.write(field.name)
+
+  if field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE:
+    # The colon is optional in this case, but our cross-language golden files
+    # don't include it.
+    out.write(': ')
+
+  PrintFieldValue(field, value, out, indent)
+  out.write('\n')
+
+def PrintFieldValue(field, value, out, indent = 0):
+  """Print a single field value (not including name).  For repeated fields,
+  the value should be a single element."""
+
+  if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:
+    out.write(' {\n')
+    PrintMessage(value, out, indent + 2)
+    out.write(' ' * indent + '}')
+  elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:
+    out.write(field.enum_type.values_by_number[value].name)
+  elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:
+    out.write('\"')
+    out.write(_CEscape(value))
+    out.write('\"')
+  elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:
+    if value:
+      out.write("true")
+    else:
+      out.write("false")
+  else:
+    out.write(str(value))
+
+# text.encode('string_escape') does not seem to satisfy our needs as it
+# encodes unprintable characters using two-digit hex escapes whereas our
+# C++ unescaping function allows hex escapes to be any length.  So,
+# "\0011".encode('string_escape') ends up being "\\x011", which will be
+# decoded in C++ as a single-character string with char code 0x11.
+def _CEscape(text):
+  def escape(c):
+    o = ord(c)
+    if o == 10: return r"\n"   # optional escape
+    if o == 13: return r"\r"   # optional escape
+    if o ==  9: return r"\t"   # optional escape
+    if o == 39: return r"\'"   # optional escape
+
+    if o == 34: return r'\"'   # necessary escape
+    if o == 92: return r"\\"   # necessary escape
+
+    if o >= 127 or o < 32: return "\\%03o" % o # necessary escapes
+    return c
+  return "".join([escape(c) for c in text])
diff --git a/gerrit_upload.py b/gerrit_upload.py
new file mode 100755
index 0000000..a49fb23
--- /dev/null
+++ b/gerrit_upload.py
@@ -0,0 +1,156 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import getpass
+import os
+import subprocess
+import sys
+from tempfile import mkstemp
+
+from codereview.proto_client import HttpRpc, Proxy
+from codereview.review_pb2 import ReviewService_Stub
+from codereview.upload_bundle_pb2 import *
+from git_command import GitCommand
+from error import UploadError
+
+try:
+  import readline
+except ImportError:
+  pass
+
+MAX_SEGMENT_SIZE = 1020 * 1024
+
+def _GetRpcServer(email, server, save_cookies):
+  """Returns an RpcServer.
+
+  Returns:
+    A new RpcServer, on which RPC calls can be made.
+  """
+
+  def GetUserCredentials():
+    """Prompts the user for a username and password."""
+    e = email
+    if e is None:
+      e = raw_input("Email: ").strip()
+    password = getpass.getpass("Password for %s: " % e)
+    return (e, password)
+
+  # If this is the dev_appserver, use fake authentication.
+  lc_server = server.lower()
+  if lc_server == "localhost" or lc_server.startswith("localhost:"):
+    if email is None:
+      email = "test@example.com"
+    server = HttpRpc(
+        server,
+        lambda: (email, "password"),
+        extra_headers={"Cookie":
+                       'dev_appserver_login="%s:False"' % email})
+    # Don't try to talk to ClientLogin.
+    server.authenticated = True
+    return server
+
+  if save_cookies:
+    cookie_file = ".gerrit_cookies"
+  else:
+    cookie_file = None
+
+  return HttpRpc(server, GetUserCredentials,
+                 cookie_file=cookie_file)
+
+def UploadBundle(project,
+                 server,
+                 email,
+                 dest_project,
+                 dest_branch,
+                 src_branch,
+                 bases,
+                 save_cookies=True):
+
+  srv = _GetRpcServer(email, server, save_cookies)
+  review = Proxy(ReviewService_Stub(srv))
+  tmp_fd, tmp_bundle = mkstemp(".bundle", ".gpq")
+  os.close(tmp_fd)
+
+  srcid = project.bare_git.rev_parse(src_branch)
+  revlist = project._revlist(src_branch, *bases)
+
+  if srcid not in revlist:
+    # This can happen if src_branch is an annotated tag
+    #
+    revlist.append(srcid)
+  revlist_size = len(revlist) * 42
+
+  try:
+    cmd = ['bundle', 'create', tmp_bundle, src_branch]
+    cmd.extend(bases)
+    if GitCommand(project, cmd).Wait() != 0:
+      raise UploadError('cannot create bundle')
+    fd = open(tmp_bundle, "rb")
+
+    bundle_id = None
+    segment_id = 0
+    next_data = fd.read(MAX_SEGMENT_SIZE - revlist_size)
+
+    while True:
+      this_data = next_data
+      next_data = fd.read(MAX_SEGMENT_SIZE)
+      segment_id += 1
+
+      if bundle_id is None:
+        req = UploadBundleRequest()
+        req.dest_project = str(dest_project)
+        req.dest_branch = str(dest_branch)
+        for c in revlist:
+          req.contained_object.append(c)
+      else:
+        req = UploadBundleContinue()
+        req.bundle_id = bundle_id
+        req.segment_id = segment_id
+
+      req.bundle_data = this_data
+      if len(next_data) > 0:
+        req.partial_upload = True
+      else:
+        req.partial_upload = False
+
+      if bundle_id is None:
+        rsp = review.UploadBundle(req)
+      else:
+        rsp = review.ContinueBundle(req)
+
+      if rsp.status_code == UploadBundleResponse.CONTINUE:
+        bundle_id = rsp.bundle_id
+      elif rsp.status_code == UploadBundleResponse.RECEIVED:
+        bundle_id = rsp.bundle_id
+        return bundle_id
+      else:
+        if rsp.status_code == UploadBundleResponse.UNKNOWN_PROJECT:
+          reason = 'unknown project "%s"' % dest_project
+        elif rsp.status_code == UploadBundleResponse.UNKNOWN_BRANCH:
+          reason = 'unknown branch "%s"' % dest_branch
+        elif rsp.status_code == UploadBundleResponse.UNKNOWN_BUNDLE:
+          reason = 'unknown bundle'
+        elif rsp.status_code == UploadBundleResponse.NOT_BUNDLE_OWNER:
+          reason = 'not bundle owner'
+        elif rsp.status_code == UploadBundleResponse.BUNDLE_CLOSED:
+          reason = 'bundle closed'
+        elif rsp.status_code == UploadBundleResponse.UNAUTHORIZED_USER:
+          reason = ('Unauthorized user.  Visit http://%s/hello to sign up.'
+                    % server)
+        else:
+          reason = 'unknown error ' + str(rsp.status_code)
+        raise UploadError(reason)
+  finally:
+    os.unlink(tmp_bundle)
diff --git a/git_command.py b/git_command.py
new file mode 100644
index 0000000..a3bd919
--- /dev/null
+++ b/git_command.py
@@ -0,0 +1,164 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import subprocess
+from error import GitError
+
+GIT = 'git'
+MIN_GIT_VERSION = (1, 5, 4)
+GIT_DIR = 'GIT_DIR'
+REPO_TRACE = 'REPO_TRACE'
+
+LAST_GITDIR = None
+LAST_CWD = None
+try:
+  TRACE = os.environ[REPO_TRACE] == '1'
+except KeyError:
+  TRACE = False
+
+
+class _GitCall(object):
+  def version(self):
+    p = GitCommand(None, ['--version'], capture_stdout=True)
+    if p.Wait() == 0:
+      return p.stdout
+    return None
+
+  def __getattr__(self, name):
+    name = name.replace('_','-')
+    def fun(*cmdv):
+      command = [name]
+      command.extend(cmdv)
+      return GitCommand(None, command).Wait() == 0
+    return fun
+git = _GitCall()
+
+class GitCommand(object):
+  def __init__(self,
+               project,
+               cmdv,
+               bare = False,
+               provide_stdin = False,
+               capture_stdout = False,
+               capture_stderr = False,
+               disable_editor = False,
+               cwd = None,
+               gitdir = None):
+    env = dict(os.environ)
+
+    for e in [REPO_TRACE,
+              GIT_DIR,
+              'GIT_ALTERNATE_OBJECT_DIRECTORIES',
+              'GIT_OBJECT_DIRECTORY',
+              'GIT_WORK_TREE',
+              'GIT_GRAFT_FILE',
+              'GIT_INDEX_FILE']:
+      if e in env:
+        del env[e]
+
+    if disable_editor:
+      env['GIT_EDITOR'] = ':'
+
+    if project:
+      if not cwd:
+        cwd = project.worktree
+      if not gitdir:
+        gitdir = project.gitdir
+
+    command = [GIT]
+    if bare:
+      if gitdir:
+        env[GIT_DIR] = gitdir
+      cwd = None
+    command.extend(cmdv)
+
+    if provide_stdin:
+      stdin = subprocess.PIPE
+    else:
+      stdin = None
+
+    if capture_stdout:
+      stdout = subprocess.PIPE
+    else:
+      stdout = None
+
+    if capture_stderr:
+      stderr = subprocess.PIPE
+    else:
+      stderr = None
+
+    if TRACE:
+      global LAST_CWD
+      global LAST_GITDIR
+
+      dbg = ''
+
+      if cwd and LAST_CWD != cwd:
+        if LAST_GITDIR or LAST_CWD:
+          dbg += '\n'
+        dbg += ': cd %s\n' % cwd
+        LAST_CWD = cwd
+
+      if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
+        if LAST_GITDIR or LAST_CWD:
+          dbg += '\n'
+        dbg += ': export GIT_DIR=%s\n' % env[GIT_DIR]
+        LAST_GITDIR = env[GIT_DIR]
+
+      dbg += ': '
+      dbg += ' '.join(command)
+      if stdin == subprocess.PIPE:
+        dbg += ' 0<|'
+      if stdout == subprocess.PIPE:
+        dbg += ' 1>|'
+      if stderr == subprocess.PIPE:
+        dbg += ' 2>|'
+      print >>sys.stderr, dbg
+
+    try:
+      p = subprocess.Popen(command,
+                           cwd = cwd,
+                           env = env,
+                           stdin = stdin,
+                           stdout = stdout,
+                           stderr = stderr)
+    except Exception, e:
+      raise GitError('%s: %s' % (command[1], e))
+
+    self.process = p
+    self.stdin = p.stdin
+
+  def Wait(self):
+    p = self.process
+
+    if p.stdin:
+      p.stdin.close()
+      self.stdin = None
+
+    if p.stdout:
+      self.stdout = p.stdout.read()
+      p.stdout.close()
+    else:
+      p.stdout = None
+
+    if p.stderr:
+      self.stderr = p.stderr.read()
+      p.stderr.close()
+    else:
+      p.stderr = None
+
+    return self.process.wait()
diff --git a/git_config.py b/git_config.py
new file mode 100644
index 0000000..f6c5bd1
--- /dev/null
+++ b/git_config.py
@@ -0,0 +1,344 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+import sys
+from error import GitError
+from git_command import GitCommand
+
+R_HEADS = 'refs/heads/'
+R_TAGS  = 'refs/tags/'
+ID_RE = re.compile('^[0-9a-f]{40}$')
+
+def IsId(rev):
+  return ID_RE.match(rev)
+
+
+class GitConfig(object):
+  @classmethod
+  def ForUser(cls):
+    return cls(file = os.path.expanduser('~/.gitconfig'))
+
+  @classmethod
+  def ForRepository(cls, gitdir, defaults=None):
+    return cls(file = os.path.join(gitdir, 'config'),
+               defaults = defaults)
+
+  def __init__(self, file, defaults=None):
+    self.file = file
+    self.defaults = defaults
+    self._cache_dict = None
+    self._remotes = {}
+    self._branches = {}
+
+  def Has(self, name, include_defaults = True):
+    """Return true if this configuration file has the key.
+    """
+    name = name.lower()
+    if name in self._cache:
+      return True
+    if include_defaults and self.defaults:
+      return self.defaults.Has(name, include_defaults = True)
+    return False
+
+  def GetBoolean(self, name):
+    """Returns a boolean from the configuration file.
+       None : The value was not defined, or is not a boolean.
+       True : The value was set to true or yes.
+       False: The value was set to false or no.
+    """
+    v = self.GetString(name)
+    if v is None:
+      return None
+    v = v.lower()
+    if v in ('true', 'yes'):
+      return True
+    if v in ('false', 'no'):
+      return False
+    return None
+
+  def GetString(self, name, all=False):
+    """Get the first value for a key, or None if it is not defined.
+
+       This configuration file is used first, if the key is not
+       defined or all = True then the defaults are also searched.
+    """
+    name = name.lower()
+
+    try:
+      v = self._cache[name]
+    except KeyError:
+      if self.defaults:
+        return self.defaults.GetString(name, all = all)
+      v = []
+
+    if not all:
+      if v:
+        return v[0]
+      return None
+
+    r = []
+    r.extend(v)
+    if self.defaults:
+      r.extend(self.defaults.GetString(name, all = True))
+    return r
+
+  def SetString(self, name, value):
+    """Set the value(s) for a key.
+       Only this configuration file is modified.
+
+       The supplied value should be either a string,
+       or a list of strings (to store multiple values).
+    """
+    name = name.lower()
+
+    try:
+      old = self._cache[name]
+    except KeyError:
+      old = []
+
+    if value is None:
+      if old:
+        del self._cache[name]
+        self._do('--unset-all', name)
+
+    elif isinstance(value, list):
+      if len(value) == 0:
+        self.SetString(name, None)
+
+      elif len(value) == 1:
+        self.SetString(name, value[0])
+
+      elif old != value:
+        self._cache[name] = list(value)
+        self._do('--replace-all', name, value[0])
+        for i in xrange(1, len(value)):
+          self._do('--add', name, value[i])
+
+    elif len(old) != 1 or old[0] != value:
+      self._cache[name] = [value]
+      self._do('--replace-all', name, value)
+
+  def GetRemote(self, name):
+    """Get the remote.$name.* configuration values as an object.
+    """
+    try:
+      r = self._remotes[name]
+    except KeyError:
+      r = Remote(self, name)
+      self._remotes[r.name] = r
+    return r
+
+  def GetBranch(self, name):
+    """Get the branch.$name.* configuration values as an object.
+    """
+    try:
+      b = self._branches[name]
+    except KeyError:
+      b = Branch(self, name)
+      self._branches[b.name] = b
+    return b
+
+  @property
+  def _cache(self):
+    if self._cache_dict is None:
+      self._cache_dict = self._Read()
+    return self._cache_dict
+
+  def _Read(self):
+    d = self._do('--null', '--list')
+    c = {}
+    while d:
+      lf = d.index('\n')
+      nul = d.index('\0', lf + 1)
+
+      key = d[0:lf]
+      val = d[lf + 1:nul]
+
+      if key in c:
+        c[key].append(val)
+      else:
+        c[key] = [val]
+
+      d = d[nul + 1:]
+    return c
+
+  def _do(self, *args):
+    command = ['config', '--file', self.file]
+    command.extend(args)
+
+    p = GitCommand(None,
+                   command,
+                   capture_stdout = True,
+                   capture_stderr = True)
+    if p.Wait() == 0:
+      return p.stdout
+    else:
+      GitError('git config %s: %s' % (str(args), p.stderr))
+
+
+class RefSpec(object):
+  """A Git refspec line, split into its components:
+
+      forced:  True if the line starts with '+'
+      src:     Left side of the line
+      dst:     Right side of the line
+  """
+
+  @classmethod
+  def FromString(cls, rs):
+    lhs, rhs = rs.split(':', 2)
+    if lhs.startswith('+'):
+      lhs = lhs[1:]
+      forced = True
+    else:
+      forced = False
+    return cls(forced, lhs, rhs)
+
+  def __init__(self, forced, lhs, rhs):
+    self.forced = forced
+    self.src = lhs
+    self.dst = rhs
+
+  def SourceMatches(self, rev):
+    if self.src:
+      if rev == self.src:
+        return True
+      if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
+        return True
+    return False
+
+  def DestMatches(self, ref):
+    if self.dst:
+      if ref == self.dst:
+        return True
+      if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
+        return True
+    return False
+
+  def MapSource(self, rev):
+    if self.src.endswith('/*'):
+      return self.dst[:-1] + rev[len(self.src) - 1:]
+    return self.dst
+
+  def __str__(self):
+    s = ''
+    if self.forced:
+      s += '+'
+    if self.src:
+      s += self.src
+    if self.dst:
+      s += ':'
+      s += self.dst
+    return s
+
+
+class Remote(object):
+  """Configuration options related to a remote.
+  """
+  def __init__(self, config, name):
+    self._config = config
+    self.name = name
+    self.url = self._Get('url')
+    self.review = self._Get('review')
+    self.fetch = map(lambda x: RefSpec.FromString(x),
+                     self._Get('fetch', all=True))
+
+  def ToLocal(self, rev):
+    """Convert a remote revision string to something we have locally.
+    """
+    if IsId(rev):
+      return rev
+    if rev.startswith(R_TAGS):
+      return rev
+
+    if not rev.startswith('refs/'):
+      rev = R_HEADS + rev
+
+    for spec in self.fetch:
+      if spec.SourceMatches(rev):
+        return spec.MapSource(rev)
+    raise GitError('remote %s does not have %s' % (self.name, rev))
+
+  def WritesTo(self, ref):
+    """True if the remote stores to the tracking ref.
+    """
+    for spec in self.fetch:
+      if spec.DestMatches(ref):
+        return True
+    return False
+
+  def ResetFetch(self):
+    """Set the fetch refspec to its default value.
+    """
+    self.fetch = [RefSpec(True,
+                          'refs/heads/*',
+                          'refs/remotes/%s/*' % self.name)]
+
+  def Save(self):
+    """Save this remote to the configuration.
+    """
+    self._Set('url', self.url)
+    self._Set('review', self.review)
+    self._Set('fetch', map(lambda x: str(x), self.fetch))
+
+  def _Set(self, key, value):
+    key = 'remote.%s.%s' % (self.name, key)
+    return self._config.SetString(key, value)
+
+  def _Get(self, key, all=False):
+    key = 'remote.%s.%s' % (self.name, key)
+    return self._config.GetString(key, all = all)
+
+
+class Branch(object):
+  """Configuration options related to a single branch.
+  """
+  def __init__(self, config, name):
+    self._config = config
+    self.name = name
+    self.merge = self._Get('merge')
+
+    r = self._Get('remote')
+    if r:
+      self.remote = self._config.GetRemote(r)
+    else:
+      self.remote = None
+
+  @property
+  def LocalMerge(self):
+    """Convert the merge spec to a local name.
+    """
+    if self.remote and self.merge:
+      return self.remote.ToLocal(self.merge)
+    return None
+
+  def Save(self):
+    """Save this branch back into the configuration.
+    """
+    self._Set('merge', self.merge)
+    if self.remote:
+      self._Set('remote', self.remote.name)
+    else:
+      self._Set('remote', None)
+
+  def _Set(self, key, value):
+    key = 'branch.%s.%s' % (self.name, key)
+    return self._config.SetString(key, value)
+
+  def _Get(self, key, all=False):
+    key = 'branch.%s.%s' % (self.name, key)
+    return self._config.GetString(key, all = all)
diff --git a/import_ext.py b/import_ext.py
new file mode 100644
index 0000000..2a1ebf8
--- /dev/null
+++ b/import_ext.py
@@ -0,0 +1,422 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import random
+import stat
+import sys
+import urllib2
+import StringIO
+
+from error import GitError, ImportError
+from git_command import GitCommand
+
+class ImportExternal(object):
+  """Imports a single revision from a non-git data source.
+     Suitable for use to import a tar or zip based snapshot.
+  """
+  def __init__(self):
+    self._marks = 0
+    self._files = {}
+    self._tempref = 'refs/repo-external/import'
+
+    self._urls = []
+    self._remap = []
+    self.parent = None
+    self._user_name = 'Upstream'
+    self._user_email = 'upstream-import@none'
+    self._user_when = 1000000
+
+    self.commit = None
+
+  def Clone(self):
+    r = self.__class__()
+
+    r.project = self.project
+    for u in self._urls:
+      r._urls.append(u)
+    for p in self._remap:
+      r._remap.append(_PathMap(r, p._old, p._new))
+
+    return r
+
+  def SetProject(self, project):
+    self.project = project
+
+  def SetVersion(self, version):
+    self.version = version
+
+  def AddUrl(self, url):
+    self._urls.append(url)
+
+  def SetParent(self, commit_hash):
+    self.parent = commit_hash
+
+  def SetCommit(self, commit_hash):
+    self.commit = commit_hash
+
+  def RemapPath(self, old, new, replace_version=True):
+    self._remap.append(_PathMap(self, old, new))
+
+  @property
+  def TagName(self):
+    v = ''
+    for c in self.version:
+      if c >= '0' and c <= '9':
+        v += c
+      elif c >= 'A' and c <= 'Z':
+        v += c
+      elif c >= 'a' and c <= 'z':
+        v += c
+      elif c in ('-', '_', '.', '/', '+', '@'):
+        v += c
+    return 'upstream/%s' % v
+
+  @property
+  def PackageName(self):
+    n = self.project.name
+    if n.startswith('platform/'):
+      # This was not my finest moment...
+      #
+      n = n[len('platform/'):]
+    return n
+
+  def Import(self):
+    self._need_graft = False
+    if self.parent:
+      try:
+        self.project.bare_git.cat_file('-e', self.parent)
+      except GitError:
+        self._need_graft = True
+
+    gfi = GitCommand(self.project,
+                     ['fast-import', '--force', '--quiet'],
+                     bare = True,
+                     provide_stdin = True)
+    try:
+      self._out = gfi.stdin
+
+      try:
+        self._UnpackFiles()
+        self._MakeCommit()
+        self._out.flush()
+      finally:
+        rc = gfi.Wait()
+      if rc != 0:
+        raise ImportError('fast-import failed')
+
+      if self._need_graft:
+        id = self._GraftCommit()
+      else:
+        id = self.project.bare_git.rev_parse('%s^0' % self._tempref)
+
+      if self.commit and self.commit != id:
+        raise ImportError('checksum mismatch: %s expected,'
+                          ' %s imported' % (self.commit, id))
+
+      self._MakeTag(id)
+      return id
+    finally:
+      try:
+        self.project.bare_git.DeleteRef(self._tempref)
+      except GitError:
+        pass
+
+  def _PickUrl(self, failed):
+    u = map(lambda x: x.replace('%version%', self.version), self._urls)
+    for f in failed:
+      if f in u:
+        u.remove(f)
+    if len(u) == 0:
+      return None
+    return random.choice(u)
+
+  def _OpenUrl(self):
+    failed = {}
+    while True:
+      url = self._PickUrl(failed.keys())
+      if url is None:
+        why = 'Cannot download %s' % self.project.name
+
+        if failed:
+          why += ': one or more mirrors are down\n'
+          bad_urls = list(failed.keys())
+          bad_urls.sort()
+          for url in bad_urls:
+            why += '  %s: %s\n' % (url, failed[url])
+        else:
+          why += ': no mirror URLs'
+        raise ImportError(why)
+
+      print >>sys.stderr, "Getting %s ..." % url
+      try:
+        return urllib2.urlopen(url), url
+      except urllib2.HTTPError, e:
+        failed[url] = e.code
+      except urllib2.URLError, e:
+        failed[url] = e.reason[1]
+      except OSError, e:
+        failed[url] = e.strerror
+
+  def _UnpackFiles(self):
+    raise NotImplementedError
+
+  def _NextMark(self):
+    self._marks += 1
+    return self._marks
+
+  def _UnpackOneFile(self, mode, size, name, fd):
+    if stat.S_ISDIR(mode):    # directory
+      return
+    else:
+      mode = self._CleanMode(mode, name)
+
+    old_name = name
+    name = self._CleanName(name)
+
+    if stat.S_ISLNK(mode) and self._remap:
+      # The link is relative to the old_name, and may need to
+      # be rewritten according to our remap rules if it goes
+      # up high enough in the tree structure.
+      #
+      dest = self._RewriteLink(fd.read(size), old_name, name)
+      fd = StringIO.StringIO(dest)
+      size = len(dest)
+
+    fi = _File(mode, name, self._NextMark())
+
+    self._out.write('blob\n')
+    self._out.write('mark :%d\n' % fi.mark)
+    self._out.write('data %d\n' % size)
+    while size > 0:
+      n = min(2048, size)
+      self._out.write(fd.read(n))
+      size -= n
+    self._out.write('\n')
+    self._files[fi.name] = fi
+
+  def _SetFileMode(self, name, mode):
+    if not stat.S_ISDIR(mode):
+      mode = self._CleanMode(mode, name)
+      name = self._CleanName(name)
+      try:
+        fi = self._files[name]
+      except KeyError:
+        raise ImportError('file %s was not unpacked' % name)
+      fi.mode = mode
+
+  def _RewriteLink(self, dest, relto_old, relto_new):
+    # Drop the last components of the symlink itself
+    # as the dest is relative to the directory its in.
+    #
+    relto_old = _TrimPath(relto_old)
+    relto_new = _TrimPath(relto_new)
+
+    # Resolve the link to be absolute from the top of
+    # the archive, so we can remap its destination.
+    #
+    while dest.find('/./') >= 0 or dest.find('//') >= 0:
+      dest = dest.replace('/./', '/')
+      dest = dest.replace('//', '/')
+
+    if dest.startswith('../') or dest.find('/../') > 0:
+      dest = _FoldPath('%s/%s' % (relto_old, dest))
+
+    for pm in self._remap:
+      if pm.Matches(dest):
+        dest = pm.Apply(dest)
+        break
+
+    dest, relto_new = _StripCommonPrefix(dest, relto_new)
+    while relto_new:
+      i = relto_new.find('/')
+      if i > 0:
+        relto_new = relto_new[i + 1:]
+      else:
+        relto_new = ''
+      dest = '../' + dest
+    return dest
+
+  def _CleanMode(self, mode, name):
+    if stat.S_ISREG(mode):  # regular file
+      if (mode & 0111) == 0:
+        return 0644
+      else:
+        return 0755
+    elif stat.S_ISLNK(mode):  # symlink
+      return stat.S_IFLNK
+    else:
+      raise ImportError('invalid mode %o in %s' % (mode, name))
+
+  def _CleanName(self, name):
+    old_name = name
+    for pm in self._remap:
+      if pm.Matches(name):
+        name = pm.Apply(name)
+        break
+    while name.startswith('/'):
+      name = name[1:]
+    if not name:
+      raise ImportError('path %s is empty after remap' % old_name)
+    if name.find('/./') >= 0 or name.find('/../') >= 0:
+      raise ImportError('path %s contains relative parts' % name)
+    return name
+
+  def _MakeCommit(self):
+    msg = '%s %s\n' % (self.PackageName, self.version)
+
+    self._out.write('commit %s\n' % self._tempref)
+    self._out.write('committer %s <%s> %d +0000\n' % (
+                    self._user_name,
+                    self._user_email,
+                    self._user_when))
+    self._out.write('data %d\n' % len(msg))
+    self._out.write(msg)
+    self._out.write('\n')
+    if self.parent and not self._need_graft:
+      self._out.write('from %s^0\n' % self.parent)
+      self._out.write('deleteall\n')
+
+    for f in self._files.values():
+      self._out.write('M %o :%d %s\n' % (f.mode, f.mark, f.name))
+    self._out.write('\n')
+
+  def _GraftCommit(self):
+    raw = self.project.bare_git.cat_file('commit', self._tempref)
+    raw = raw.split("\n")
+    while raw[1].startswith('parent '):
+      del raw[1]
+    raw.insert(1, 'parent %s' % self.parent)
+    id = self._WriteObject('commit', "\n".join(raw))
+
+    graft_file = os.path.join(self.project.gitdir, 'info/grafts')
+    if os.path.exists(graft_file):
+      graft_list = open(graft_file, 'rb').read().split("\n")
+      if graft_list and graft_list[-1] == '':
+        del graft_list[-1]
+    else:
+      graft_list = []
+
+    exists = False
+    for line in graft_list:
+      if line == id:
+        exists = True
+        break
+
+    if not exists:
+      graft_list.append(id)
+      graft_list.append('')
+      fd = open(graft_file, 'wb')
+      fd.write("\n".join(graft_list))
+      fd.close()
+
+    return id
+
+  def _MakeTag(self, id):
+    name = self.TagName
+
+    raw = []
+    raw.append('object %s' % id)
+    raw.append('type commit')
+    raw.append('tag %s' % name)
+    raw.append('tagger %s <%s> %d +0000' % (
+      self._user_name,
+      self._user_email,
+      self._user_when))
+    raw.append('')
+    raw.append('%s %s\n' % (self.PackageName, self.version))
+
+    tagid = self._WriteObject('tag', "\n".join(raw))
+    self.project.bare_git.UpdateRef('refs/tags/%s' % name, tagid)
+
+  def _WriteObject(self, type, data):
+    wo = GitCommand(self.project,
+                    ['hash-object', '-t', type, '-w', '--stdin'],
+                    bare = True,
+                    provide_stdin = True,
+                    capture_stdout = True,
+                    capture_stderr = True)
+    wo.stdin.write(data)
+    if wo.Wait() != 0:
+      raise GitError('cannot create %s from (%s)' % (type, data))
+    return wo.stdout[:-1]
+
+
+def _TrimPath(path):
+  i = path.rfind('/')
+  if i > 0:
+    path = path[0:i]
+  return ''
+
+def _StripCommonPrefix(a, b):
+  while True:
+    ai = a.find('/')
+    bi = b.find('/')
+    if ai > 0 and bi > 0 and a[0:ai] == b[0:bi]:
+      a = a[ai + 1:]
+      b = b[bi + 1:]
+    else:
+      break
+  return a, b
+
+def _FoldPath(path):
+  while True:
+    if path.startswith('../'):
+      return path
+
+    i = path.find('/../')
+    if i <= 0:
+      if path.startswith('/'):
+        return path[1:]
+      return path
+
+    lhs = path[0:i]
+    rhs = path[i + 4:]
+
+    i = lhs.rfind('/')
+    if i > 0:
+      path = lhs[0:i + 1] + rhs
+    else:
+      path = rhs
+
+class _File(object):
+  def __init__(self, mode, name, mark):
+    self.mode = mode
+    self.name = name
+    self.mark = mark
+
+
+class _PathMap(object):
+  def __init__(self, imp, old, new):
+    self._imp = imp
+    self._old = old
+    self._new = new
+
+  def _r(self, p):
+    return p.replace('%version%', self._imp.version)
+
+  @property
+  def old(self):
+    return self._r(self._old)
+
+  @property
+  def new(self):
+    return self._r(self._new)
+
+  def Matches(self, name):
+    return name.startswith(self.old)
+
+  def Apply(self, name):
+    return self.new + name[len(self.old):]
diff --git a/import_tar.py b/import_tar.py
new file mode 100644
index 0000000..d7ce14d
--- /dev/null
+++ b/import_tar.py
@@ -0,0 +1,206 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import bz2
+import stat
+import tarfile
+import zlib
+import StringIO
+
+from import_ext import ImportExternal
+from error import ImportError
+
+class ImportTar(ImportExternal):
+  """Streams a (optionally compressed) tar file from the network
+     directly into a Project's Git repository.
+  """
+  @classmethod
+  def CanAccept(cls, url):
+    """Can this importer read and unpack the data stored at url?
+    """
+    if url.endswith('.tar.gz') or url.endswith('.tgz'):
+      return True
+    if url.endswith('.tar.bz2'):
+      return True
+    if url.endswith('.tar'):
+      return True
+    return False
+
+  def _UnpackFiles(self):
+    url_fd, url = self._OpenUrl()
+    try:
+      if url.endswith('.tar.gz') or url.endswith('.tgz'):
+        tar_fd = _Gzip(url_fd)
+      elif url.endswith('.tar.bz2'):
+        tar_fd = _Bzip2(url_fd)
+      elif url.endswith('.tar'):
+        tar_fd = _Raw(url_fd)
+      else:
+        raise ImportError('non-tar file extension: %s' % url)
+
+      try:
+        tar = tarfile.TarFile(name = url,
+                              mode = 'r',
+                              fileobj = tar_fd)
+        try:
+          for entry in tar:
+            mode = entry.mode
+
+            if (mode & 0170000) == 0:
+              if entry.isdir():
+                mode |= stat.S_IFDIR
+              elif entry.isfile() or entry.islnk():  # hard links as files
+                mode |= stat.S_IFREG
+              elif entry.issym():
+                mode |= stat.S_IFLNK
+
+            if stat.S_ISLNK(mode):   # symlink
+              data_fd = StringIO.StringIO(entry.linkname)
+              data_sz = len(entry.linkname)
+            elif stat.S_ISDIR(mode): # directory
+              data_fd = StringIO.StringIO('')
+              data_sz = 0
+            else:
+              data_fd = tar.extractfile(entry)
+              data_sz = entry.size
+
+            self._UnpackOneFile(mode, data_sz, entry.name, data_fd)
+        finally:
+          tar.close()
+      finally:
+        tar_fd.close()
+    finally:
+      url_fd.close()
+
+
+
+class _DecompressStream(object):
+  """file like object to decompress a tar stream
+  """
+  def __init__(self, fd):
+    self._fd = fd
+    self._pos = 0
+    self._buf = None
+
+  def tell(self):
+    return self._pos
+
+  def seek(self, offset):
+    d = offset - self._pos
+    if d > 0:
+      self.read(d)
+    elif d == 0:
+      pass
+    else:
+      raise NotImplementedError, 'seek backwards'
+
+  def close(self):
+    self._fd = None
+
+  def read(self, size = -1):
+    if not self._fd:
+      raise EOFError, 'Reached EOF'
+    
+    r = []
+    try:
+      if size >= 0:
+        self._ReadChunk(r, size)
+      else:
+        while True:
+          self._ReadChunk(r, 2048)
+    except EOFError:
+      pass
+
+    if len(r) == 1:
+      r = r[0]
+    else:
+      r = ''.join(r)
+    self._pos += len(r)
+    return r
+
+  def _ReadChunk(self, r, size):
+    b = self._buf
+    try:
+      while size > 0:
+        if b is None or len(b) == 0:
+          b = self._Decompress(self._fd.read(2048))
+          continue
+
+        use = min(size, len(b))
+        r.append(b[:use])
+        b = b[use:]
+        size -= use
+    finally:
+      self._buf = b
+
+  def _Decompress(self, b):
+    raise NotImplementedError, '_Decompress'
+
+
+class _Raw(_DecompressStream):
+  """file like object for an uncompressed stream
+  """
+  def __init__(self, fd):
+    _DecompressStream.__init__(self, fd)
+
+  def _Decompress(self, b):
+    return b
+
+
+class _Bzip2(_DecompressStream):
+  """file like object to decompress a .bz2 stream
+  """
+  def __init__(self, fd):
+    _DecompressStream.__init__(self, fd)
+    self._bz = bz2.BZ2Decompressor()
+
+  def _Decompress(self, b):
+    return self._bz.decompress(b)
+
+
+_FHCRC, _FEXTRA, _FNAME, _FCOMMENT = 2, 4, 8, 16
+class _Gzip(_DecompressStream):
+  """file like object to decompress a .gz stream
+  """
+  def __init__(self, fd):
+    _DecompressStream.__init__(self, fd)
+    self._z = zlib.decompressobj(-zlib.MAX_WBITS)
+
+    magic = fd.read(2)
+    if magic != '\037\213':
+      raise IOError, 'Not a gzipped file'
+
+    method = ord(fd.read(1))
+    if method != 8:
+      raise IOError, 'Unknown compression method'
+
+    flag = ord(fd.read(1))
+    fd.read(6)
+
+    if flag & _FEXTRA:
+      xlen = ord(fd.read(1))
+      xlen += 256 * ord(fd.read(1))
+      fd.read(xlen)
+    if flag & _FNAME:
+      while fd.read(1) != '\0':
+        pass
+    if flag & _FCOMMENT:
+      while fd.read(1) != '\0':
+        pass
+    if flag & _FHCRC:
+      fd.read(2)
+
+  def _Decompress(self, b):
+    return self._z.decompress(b)
diff --git a/import_zip.py b/import_zip.py
new file mode 100644
index 0000000..08aff32
--- /dev/null
+++ b/import_zip.py
@@ -0,0 +1,345 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import stat
+import struct
+import zlib
+import cStringIO
+
+from import_ext import ImportExternal
+from error import ImportError
+
+class ImportZip(ImportExternal):
+  """Streams a zip file from the network directly into a Project's
+     Git repository.
+  """
+  @classmethod
+  def CanAccept(cls, url):
+    """Can this importer read and unpack the data stored at url?
+    """
+    if url.endswith('.zip') or url.endswith('.jar'):
+      return True
+    return False
+
+  def _UnpackFiles(self):
+    url_fd, url = self._OpenUrl()
+    try:
+      if not self.__class__.CanAccept(url):
+        raise ImportError('non-zip file extension: %s' % url)
+
+      zip = _ZipFile(url_fd)
+      for entry in zip.FileRecords():
+        data = zip.Open(entry).read()
+        sz = len(data)
+
+        if data and _SafeCRLF(data):
+          data = data.replace('\r\n', '\n')
+          sz = len(data)
+
+        fd = cStringIO.StringIO(data)
+        self._UnpackOneFile(entry.mode, sz, entry.name, fd)
+        zip.Close(entry)
+
+      for entry in zip.CentralDirectory():
+        self._SetFileMode(entry.name, entry.mode)
+
+      zip.CheckTail()
+    finally:
+      url_fd.close()
+
+
+def _SafeCRLF(data):
+  """Is it reasonably safe to perform a CRLF->LF conversion?
+
+     If the stream contains a NUL byte it is likely binary,
+     and thus a CRLF->LF conversion may damage the stream.
+
+     If the only NUL is in the last position of the stream,
+     but it otherwise can do a CRLF<->LF conversion we do
+     the CRLF conversion anyway.  At least one source ZIP
+     file has this structure in its source code.
+
+     If every occurrance of a CR and LF is paired up as a
+     CRLF pair then the conversion is safely bi-directional.
+     s/\r\n/\n/g == s/\n/\r\\n/g can convert between them.
+  """
+  nul = data.find('\0')
+  if 0 <= nul and nul < (len(data) - 1):
+    return False
+
+  n_lf = 0
+  last = 0
+  while True:
+    lf = data.find('\n', last)
+    if lf < 0:
+      break
+    if lf == 0 or data[lf - 1] != '\r':
+      return False
+    last = lf + 1
+    n_lf += 1
+  return n_lf > 0
+
+class _ZipFile(object):
+  """Streaming iterator to parse a zip file on the fly.
+  """
+  def __init__(self, fd):
+    self._fd = _UngetStream(fd)
+
+  def FileRecords(self):
+    return _FileIter(self._fd)
+
+  def CentralDirectory(self):
+    return _CentIter(self._fd)
+
+  def CheckTail(self):
+    type_buf = self._fd.read(4)
+    type = struct.unpack('<I', type_buf)[0]
+    if type != 0x06054b50:  # end of central directory
+      raise ImportError('zip record %x unsupported' % type)
+
+  def Open(self, entry):
+    if entry.is_compressed:
+      return _InflateStream(self._fd)
+    else:
+      if entry.has_trailer:
+        raise ImportError('unable to extract streamed zip')
+      return _FixedLengthStream(self._fd, entry.uncompressed_size)
+
+  def Close(self, entry):
+    if entry.has_trailer:
+      type = struct.unpack('<I', self._fd.read(4))[0]
+      if type == 0x08074b50:
+        # Not a formal type marker, but commonly seen in zips
+        # as the data descriptor signature.
+        #
+        struct.unpack('<3I', self._fd.read(12))
+      else:
+        # No signature for the data descriptor, so read the
+        # remaining fields out of the stream
+        #
+        self._fd.read(8)
+
+
+class _FileIter(object):
+  def __init__(self, fd):
+    self._fd = fd
+
+  def __iter__(self):
+    return self
+
+  def next(self):
+    fd = self._fd
+
+    type_buf = fd.read(4)
+    type = struct.unpack('<I', type_buf)[0]
+
+    if type != 0x04034b50:    # local file header
+      fd.unread(type_buf)
+      raise StopIteration()
+
+    rec = _FileHeader(fd.read(26))
+    rec.name = fd.read(rec.name_len)
+    fd.read(rec.extra_len)
+
+    if rec.name.endswith('/'):
+      rec.name = rec.name[:-1]
+      rec.mode = stat.S_IFDIR | 0777
+    return rec
+
+
+class _FileHeader(object):
+  """Information about a single file in the archive.
+     0  version needed to extract       2 bytes
+     1  general purpose bit flag        2 bytes
+     2  compression method              2 bytes
+     3  last mod file time              2 bytes
+     4  last mod file date              2 bytes
+     5  crc-32                          4 bytes
+     6  compressed size                 4 bytes
+     7  uncompressed size               4 bytes
+     8  file name length                2 bytes
+     9  extra field length              2 bytes
+  """
+  def __init__(self, raw_bin):
+    rec = struct.unpack('<5H3I2H', raw_bin)
+    
+    if rec[2] == 8:
+      self.is_compressed = True
+    elif rec[2] == 0:
+      self.is_compressed = False
+    else:
+      raise ImportError('unrecognized compression format')
+
+    if rec[1] & (1 << 3):
+      self.has_trailer = True
+    else:
+      self.has_trailer = False
+
+    self.compressed_size  = rec[6]
+    self.uncompressed_size = rec[7]
+    self.name_len = rec[8]
+    self.extra_len = rec[9]
+    self.mode = stat.S_IFREG | 0644
+
+
+class _CentIter(object):
+  def __init__(self, fd):
+    self._fd = fd
+
+  def __iter__(self):
+    return self
+
+  def next(self):
+    fd = self._fd
+
+    type_buf = fd.read(4)
+    type = struct.unpack('<I', type_buf)[0]
+
+    if type != 0x02014b50:  # central directory
+      fd.unread(type_buf)
+      raise StopIteration()
+
+    rec = _CentHeader(fd.read(42))
+    rec.name = fd.read(rec.name_len)
+    fd.read(rec.extra_len)
+    fd.read(rec.comment_len)
+
+    if rec.name.endswith('/'):
+      rec.name = rec.name[:-1]
+      rec.mode = stat.S_IFDIR | 0777
+    return rec
+
+
+class _CentHeader(object):
+  """Information about a single file in the archive.
+     0  version made by                 2 bytes
+     1  version needed to extract       2 bytes
+     2  general purpose bit flag        2 bytes
+     3  compression method              2 bytes
+     4  last mod file time              2 bytes
+     5  last mod file date              2 bytes
+     6  crc-32                          4 bytes
+     7  compressed size                 4 bytes
+     8  uncompressed size               4 bytes
+     9  file name length                2 bytes
+    10  extra field length              2 bytes
+    11  file comment length             2 bytes
+    12  disk number start               2 bytes
+    13  internal file attributes        2 bytes
+    14  external file attributes        4 bytes
+    15  relative offset of local header 4 bytes
+  """
+  def __init__(self, raw_bin):
+    rec = struct.unpack('<6H3I5H2I', raw_bin)
+    self.name_len = rec[9]
+    self.extra_len = rec[10]
+    self.comment_len = rec[11]
+
+    if (rec[0] & 0xff00) == 0x0300:  # UNIX
+      self.mode = rec[14] >> 16
+    else:
+      self.mode = stat.S_IFREG | 0644
+
+
+class _UngetStream(object):
+  """File like object to read and rewind a stream.
+  """
+  def __init__(self, fd):
+    self._fd = fd
+    self._buf = None
+
+  def read(self, size = -1):
+    r = []
+    try:
+      if size >= 0:
+        self._ReadChunk(r, size)
+      else:
+        while True:
+          self._ReadChunk(r, 2048)
+    except EOFError:
+      pass
+
+    if len(r) == 1:
+      return r[0]
+    return ''.join(r)
+
+  def unread(self, buf):
+    b = self._buf
+    if b is None or len(b) == 0:
+      self._buf = buf
+    else:
+      self._buf = buf + b
+
+  def _ReadChunk(self, r, size):
+    b = self._buf
+    try:
+      while size > 0:
+        if b is None or len(b) == 0:
+          b = self._Inflate(self._fd.read(2048))
+          if not b:
+            raise EOFError()
+          continue
+
+        use = min(size, len(b))
+        r.append(b[:use])
+        b = b[use:]
+        size -= use
+    finally:
+      self._buf = b
+
+  def _Inflate(self, b):
+    return b
+
+
+class _FixedLengthStream(_UngetStream):
+  """File like object to read a fixed length stream.
+  """
+  def __init__(self, fd, have):
+    _UngetStream.__init__(self, fd)
+    self._have = have
+
+  def _Inflate(self, b):
+    n = self._have
+    if n == 0:
+      self._fd.unread(b)
+      return None
+
+    if len(b) > n:
+      self._fd.unread(b[n:])
+      b = b[:n]
+    self._have -= len(b)
+    return b
+
+
+class _InflateStream(_UngetStream):
+  """Inflates the stream as it reads input.
+  """
+  def __init__(self, fd):
+    _UngetStream.__init__(self, fd)
+    self._z = zlib.decompressobj(-zlib.MAX_WBITS)
+
+  def _Inflate(self, b):
+    z = self._z
+    if not z:
+      self._fd.unread(b)
+      return None
+
+    b = z.decompress(b)
+    if z.unconsumed_tail != '':
+      self._fd.unread(z.unconsumed_tail)
+    elif z.unused_data != '':
+      self._fd.unread(z.unused_data)
+      self._z = None
+    return b
diff --git a/main.py b/main.py
new file mode 100755
index 0000000..5609299
--- /dev/null
+++ b/main.py
@@ -0,0 +1,198 @@
+#!/bin/sh
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+magic='--calling-python-from-/bin/sh--'
+"""exec" python2.4 -E "$0" "$@" """#$magic"
+if __name__ == '__main__':
+  import sys
+  if sys.argv[-1] == '#%s' % magic:
+    del sys.argv[-1]
+del magic
+
+import optparse
+import os
+import re
+import sys
+
+from command import InteractiveCommand, PagedCommand
+from error import NoSuchProjectError
+from error import RepoChangedException
+from manifest import Manifest
+from pager import RunPager
+
+from subcmds import all as all_commands
+
+global_options = optparse.OptionParser(
+                 usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]"
+                 )
+global_options.add_option('-p', '--paginate',
+                          dest='pager', action='store_true',
+                          help='display command output in the pager')
+global_options.add_option('--no-pager',
+                          dest='no_pager', action='store_true',
+                          help='disable the pager')
+
+class _Repo(object):
+  def __init__(self, repodir):
+    self.repodir = repodir
+    self.commands = all_commands
+
+  def _Run(self, argv):
+    name = None
+    glob = []
+
+    for i in xrange(0, len(argv)):
+      if not argv[i].startswith('-'):
+        name = argv[i]
+        if i > 0:
+          glob = argv[:i]
+        argv = argv[i + 1:]
+        break
+    if not name:
+      glob = argv
+      name = 'help'
+      argv = []
+    gopts, gargs = global_options.parse_args(glob)
+
+    try:
+      cmd = self.commands[name]
+    except KeyError:
+      print >>sys.stderr,\
+            "repo: '%s' is not a repo command.  See 'repo help'."\
+            % name
+      sys.exit(1)
+
+    cmd.repodir = self.repodir
+    cmd.manifest = Manifest(cmd.repodir)
+
+    if not gopts.no_pager and not isinstance(cmd, InteractiveCommand):
+      config = cmd.manifest.globalConfig
+      if gopts.pager:
+        use_pager = True
+      else:
+        use_pager = config.GetBoolean('pager.%s' % name)
+        if use_pager is None:
+          use_pager = isinstance(cmd, PagedCommand)
+      if use_pager:
+        RunPager(config)
+
+    copts, cargs = cmd.OptionParser.parse_args(argv)
+    try:
+      cmd.Execute(copts, cargs)
+    except NoSuchProjectError, e:
+      if e.name:
+        print >>sys.stderr, 'error: project %s not found' % e.name
+      else:
+        print >>sys.stderr, 'error: no project in current directory'
+      sys.exit(1)
+
+def _MyWrapperPath():
+  return os.path.join(os.path.dirname(__file__), 'repo')
+
+def _CurrentWrapperVersion():
+  VERSION = None
+  pat = re.compile(r'^VERSION *=')
+  fd = open(_MyWrapperPath())
+  for line in fd:
+    if pat.match(line):
+      fd.close()
+      exec line
+      return VERSION
+  raise NameError, 'No VERSION in repo script'
+
+def _CheckWrapperVersion(ver, repo_path):
+  if not repo_path:
+    repo_path = '~/bin/repo'
+
+  if not ver:
+     print >>sys.stderr, 'no --wrapper-version argument'
+     sys.exit(1)
+
+  exp = _CurrentWrapperVersion()
+  ver = tuple(map(lambda x: int(x), ver.split('.')))
+  if len(ver) == 1:
+    ver = (0, ver[0])
+
+  if exp[0] > ver[0] or ver < (0, 4):
+    exp_str = '.'.join(map(lambda x: str(x), exp))
+    print >>sys.stderr, """
+!!! A new repo command (%5s) is available.    !!!
+!!! You must upgrade before you can continue:   !!!
+
+    cp %s %s
+""" % (exp_str, _MyWrapperPath(), repo_path)
+    sys.exit(1)
+
+  if exp > ver:
+    exp_str = '.'.join(map(lambda x: str(x), exp))
+    print >>sys.stderr, """
+... A new repo command (%5s) is available.
+... You should upgrade soon:
+
+    cp %s %s
+""" % (exp_str, _MyWrapperPath(), repo_path)
+
+def _CheckRepoDir(dir):
+  if not dir:
+     print >>sys.stderr, 'no --repo-dir argument'
+     sys.exit(1)
+
+def _PruneOptions(argv, opt):
+  i = 0
+  while i < len(argv):
+    a = argv[i]
+    if a == '--':
+      break
+    if a.startswith('--'):
+      eq = a.find('=')
+      if eq > 0:
+        a = a[0:eq]
+    if not opt.has_option(a):
+      del argv[i]
+      continue
+    i += 1
+
+def _Main(argv):
+  opt = optparse.OptionParser(usage="repo wrapperinfo -- ...")
+  opt.add_option("--repo-dir", dest="repodir",
+                 help="path to .repo/")
+  opt.add_option("--wrapper-version", dest="wrapper_version",
+                 help="version of the wrapper script")
+  opt.add_option("--wrapper-path", dest="wrapper_path",
+                 help="location of the wrapper script")
+  _PruneOptions(argv, opt)
+  opt, argv = opt.parse_args(argv)
+
+  _CheckWrapperVersion(opt.wrapper_version, opt.wrapper_path)
+  _CheckRepoDir(opt.repodir)
+
+  repo = _Repo(opt.repodir)
+  try:
+    repo._Run(argv)
+  except KeyboardInterrupt:
+    sys.exit(1)
+  except RepoChangedException:
+    # If the repo or manifest changed, re-exec ourselves.
+    #
+    try:
+      os.execv(__file__, sys.argv)
+    except OSError, e:
+      print >>sys.stderr, 'fatal: cannot restart repo after upgrade'
+      print >>sys.stderr, 'fatal: %s' % e
+      sys.exit(128)
+
+if __name__ == '__main__':
+  _Main(sys.argv[1:])
diff --git a/manifest.py b/manifest.py
new file mode 100644
index 0000000..45b0f9a
--- /dev/null
+++ b/manifest.py
@@ -0,0 +1,338 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import xml.dom.minidom
+
+from editor import Editor
+from git_config import GitConfig, IsId
+from import_tar import ImportTar
+from import_zip import ImportZip
+from project import Project, MetaProject, R_TAGS
+from remote import Remote
+from error import ManifestParseError
+
+MANIFEST_FILE_NAME = 'manifest.xml'
+
+class _Default(object):
+  """Project defaults within the manifest."""
+
+  revision = None
+  remote = None
+
+
+class Manifest(object):
+  """manages the repo configuration file"""
+
+  def __init__(self, repodir):
+    self.repodir = os.path.abspath(repodir)
+    self.topdir = os.path.dirname(self.repodir)
+    self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME)
+
+    self.globalConfig = GitConfig.ForUser()
+    Editor.globalConfig = self.globalConfig
+
+    self.repoProject = MetaProject(self, 'repo',
+      gitdir   = os.path.join(repodir, 'repo/.git'),
+      worktree = os.path.join(repodir, 'repo'))
+
+    wt     = os.path.join(repodir, 'manifests')
+    gd_new = os.path.join(repodir, 'manifests.git')
+    gd_old = os.path.join(wt, '.git')
+    if os.path.exists(gd_new) or not os.path.exists(gd_old):
+      gd = gd_new
+    else:
+      gd = gd_old
+    self.manifestProject = MetaProject(self, 'manifests',
+      gitdir   = gd,
+      worktree = wt)
+
+    self._Unload()
+
+  def Link(self, name):
+    """Update the repo metadata to use a different manifest.
+    """
+    path = os.path.join(self.manifestProject.worktree, name)
+    if not os.path.isfile(path):
+      raise ManifestParseError('manifest %s not found' % name)
+
+    old = self.manifestFile
+    try:
+      self.manifestFile = path
+      self._Unload()
+      self._Load()
+    finally:
+      self.manifestFile = old
+
+    try:
+      if os.path.exists(self.manifestFile):
+        os.remove(self.manifestFile)
+      os.symlink('manifests/%s' % name, self.manifestFile)
+    except OSError, e:
+      raise ManifestParseError('cannot link manifest %s' % name)
+
+  @property
+  def projects(self):
+    self._Load()
+    return self._projects
+
+  @property
+  def remotes(self):
+    self._Load()
+    return self._remotes
+
+  @property
+  def default(self):
+    self._Load()
+    return self._default
+
+  def _Unload(self):
+    self._loaded = False
+    self._projects = {}
+    self._remotes = {}
+    self._default = None
+    self.branch = None
+
+  def _Load(self):
+    if not self._loaded:
+      self._ParseManifest()
+      self._loaded = True
+
+  def _ParseManifest(self):
+    root = xml.dom.minidom.parse(self.manifestFile)
+    if not root or not root.childNodes:
+      raise ManifestParseError, \
+            "no root node in %s" % \
+            self.manifestFile
+
+    config = root.childNodes[0]
+    if config.nodeName != 'manifest':
+      raise ManifestParseError, \
+            "no <manifest> in %s" % \
+            self.manifestFile
+
+    self.branch = config.getAttribute('branch')
+    if not self.branch:
+      self.branch = 'default'
+
+    for node in config.childNodes:
+      if node.nodeName == 'remote':
+        remote = self._ParseRemote(node)
+        if self._remotes.get(remote.name):
+          raise ManifestParseError, \
+                'duplicate remote %s in %s' % \
+                (remote.name, self.manifestFile)
+        self._remotes[remote.name] = remote
+
+    for node in config.childNodes:
+      if node.nodeName == 'default':
+        if self._default is not None:
+          raise ManifestParseError, \
+                'duplicate default in %s' % \
+                (self.manifestFile)
+        self._default = self._ParseDefault(node)
+    if self._default is None:
+      self._default = _Default()
+
+    for node in config.childNodes:
+      if node.nodeName == 'project':
+        project = self._ParseProject(node)
+        if self._projects.get(project.name):
+          raise ManifestParseError, \
+                'duplicate project %s in %s' % \
+                (project.name, self.manifestFile)
+        self._projects[project.name] = project
+
+  def _ParseRemote(self, node):
+    """
+    reads a <remote> element from the manifest file
+    """
+    name = self._reqatt(node, 'name')
+    fetch = self._reqatt(node, 'fetch')
+    review = node.getAttribute('review')
+
+    r = Remote(name=name,
+               fetch=fetch,
+               review=review)
+
+    for n in node.childNodes:
+      if n.nodeName == 'require':
+        r.requiredCommits.append(self._reqatt(n, 'commit'))
+
+    return r
+
+  def _ParseDefault(self, node):
+    """
+    reads a <default> element from the manifest file
+    """
+    d = _Default()
+    d.remote = self._get_remote(node)
+    d.revision = node.getAttribute('revision')
+    return d
+
+  def _ParseProject(self, node):
+    """
+    reads a <project> element from the manifest file
+    """ 
+    name = self._reqatt(node, 'name')
+
+    remote = self._get_remote(node)
+    if remote is None:
+      remote = self._default.remote
+    if remote is None:
+      raise ManifestParseError, \
+            "no remote for project %s within %s" % \
+            (name, self.manifestFile)
+
+    revision = node.getAttribute('revision')
+    if not revision:
+      revision = self._default.revision
+    if not revision:
+      raise ManifestParseError, \
+            "no revision for project %s within %s" % \
+            (name, self.manifestFile)
+
+    path = node.getAttribute('path')
+    if not path:
+      path = name
+    if path.startswith('/'):
+      raise ManifestParseError, \
+            "project %s path cannot be absolute in %s" % \
+            (name, self.manifestFile)
+
+    worktree = os.path.join(self.topdir, path)
+    gitdir = os.path.join(self.repodir, 'projects/%s.git' % path)
+
+    project = Project(manifest = self,
+                      name = name,
+                      remote = remote,
+                      gitdir = gitdir,
+                      worktree = worktree,
+                      relpath = path,
+                      revision = revision)
+
+    for n in node.childNodes:
+      if n.nodeName == 'remote':
+        r = self._ParseRemote(n)
+        if project.extraRemotes.get(r.name) \
+           or project.remote.name == r.name:
+          raise ManifestParseError, \
+                'duplicate remote %s in project %s in %s' % \
+                (r.name, project.name, self.manifestFile)
+        project.extraRemotes[r.name] = r
+      elif n.nodeName == 'copyfile':
+        self._ParseCopyFile(project, n)
+
+    to_resolve = []
+    by_version = {}
+
+    for n in node.childNodes:
+      if n.nodeName == 'import':
+        self._ParseImport(project, n, to_resolve, by_version)
+
+    for pair in to_resolve:
+      sn, pr = pair
+      try:
+        sn.SetParent(by_version[pr].commit)
+      except KeyError:
+        raise ManifestParseError, \
+              'snapshot %s not in project %s in %s' % \
+              (pr, project.name, self.manifestFile)
+
+    return project
+
+  def _ParseImport(self, project, import_node, to_resolve, by_version):
+    first_url = None
+    for node in import_node.childNodes:
+      if node.nodeName == 'mirror':
+        first_url = self._reqatt(node, 'url')
+        break
+    if not first_url:
+      raise ManifestParseError, \
+            'mirror url required for project %s in %s' % \
+            (project.name, self.manifestFile)
+
+    imp = None
+    for cls in [ImportTar, ImportZip]:
+      if cls.CanAccept(first_url):
+        imp = cls()
+        break
+    if not imp:
+      raise ManifestParseError, \
+            'snapshot %s unsupported for project %s in %s' % \
+            (first_url, project.name, self.manifestFile)
+
+    imp.SetProject(project)
+
+    for node in import_node.childNodes:
+      if node.nodeName == 'remap':
+        old = node.getAttribute('strip')
+        new = node.getAttribute('insert')
+        imp.RemapPath(old, new)
+
+      elif node.nodeName == 'mirror':
+        imp.AddUrl(self._reqatt(node, 'url'))
+
+    for node in import_node.childNodes:
+      if node.nodeName == 'snapshot':
+        sn = imp.Clone()
+        sn.SetVersion(self._reqatt(node, 'version'))
+        sn.SetCommit(node.getAttribute('check'))
+
+        pr = node.getAttribute('prior')
+        if pr:
+          if IsId(pr):
+            sn.SetParent(pr)
+          else:
+            to_resolve.append((sn, pr))
+
+        rev = R_TAGS + sn.TagName
+
+        if rev in project.snapshots:
+          raise ManifestParseError, \
+                'duplicate snapshot %s for project %s in %s' % \
+                (sn.version, project.name, self.manifestFile)
+        project.snapshots[rev] = sn
+        by_version[sn.version] = sn
+
+  def _ParseCopyFile(self, project, node):
+    src = self._reqatt(node, 'src')
+    dest = self._reqatt(node, 'dest')
+    # src is project relative, and dest is relative to the top of the tree
+    project.AddCopyFile(src, os.path.join(self.topdir, dest))
+
+  def _get_remote(self, node):
+    name = node.getAttribute('remote')
+    if not name:
+      return None
+
+    v = self._remotes.get(name)
+    if not v:
+      raise ManifestParseError, \
+            "remote %s not defined in %s" % \
+            (name, self.manifestFile)
+    return v
+
+  def _reqatt(self, node, attname):
+    """
+    reads a required attribute from the node.
+    """
+    v = node.getAttribute(attname)
+    if not v:
+      raise ManifestParseError, \
+            "no %s in <%s> within %s" % \
+            (attname, node.nodeName, self.manifestFile)
+    return v
diff --git a/pager.py b/pager.py
new file mode 100755
index 0000000..320131c
--- /dev/null
+++ b/pager.py
@@ -0,0 +1,84 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import select
+import sys
+
+active = False
+
+def RunPager(globalConfig):
+  global active
+
+  if not os.isatty(0):
+    return
+  pager = _SelectPager(globalConfig)
+  if pager == '' or pager == 'cat':
+    return
+
+  # This process turns into the pager; a child it forks will
+  # do the real processing and output back to the pager. This
+  # is necessary to keep the pager in control of the tty.
+  #
+  try:
+    r, w = os.pipe()
+    pid = os.fork()
+    if not pid:
+      os.dup2(w, 1)
+      os.dup2(w, 2)
+      os.close(r)
+      os.close(w)
+      active = True
+      return
+
+    os.dup2(r, 0)
+    os.close(r)
+    os.close(w)
+
+    _BecomePager(pager)
+  except Exception:
+    print >>sys.stderr, "fatal: cannot start pager '%s'" % pager
+    os.exit(255)
+
+def _SelectPager(globalConfig):
+  try:
+    return os.environ['GIT_PAGER']
+  except KeyError:
+    pass
+
+  pager = globalConfig.GetString('core.pager')
+  if pager:
+    return pager
+
+  try:
+    return os.environ['PAGER']
+  except KeyError:
+    pass
+
+  return 'less'
+
+def _BecomePager(pager):
+  # Delaying execution of the pager until we have output
+  # ready works around a long-standing bug in popularly
+  # available versions of 'less', a better 'more'.
+  #
+  a, b, c = select.select([0], [], [0])
+
+  os.environ['LESS'] = 'FRSX'
+
+  try:
+    os.execvp(pager, [pager])
+  except OSError, e:
+    os.execv('/bin/sh', ['sh', '-c', pager])
diff --git a/project.py b/project.py
new file mode 100644
index 0000000..7c0c58f
--- /dev/null
+++ b/project.py
@@ -0,0 +1,1058 @@
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import filecmp
+import os
+import re
+import shutil
+import stat
+import sys
+import urllib2
+
+from color import Coloring
+from git_command import GitCommand
+from git_config import GitConfig, IsId
+from gerrit_upload import UploadBundle
+from error import GitError, ImportError, UploadError
+from remote import Remote
+from codereview import proto_client
+
+HEAD    = 'HEAD'
+R_HEADS = 'refs/heads/'
+R_TAGS  = 'refs/tags/'
+R_PUB   = 'refs/published/'
+R_M     = 'refs/remotes/m/'
+
+def _warn(fmt, *args):
+  msg = fmt % args
+  print >>sys.stderr, 'warn: %s' % msg
+
+def _info(fmt, *args):
+  msg = fmt % args
+  print >>sys.stderr, 'info: %s' % msg
+
+def not_rev(r):
+  return '^' + r
+
+class ReviewableBranch(object):
+  _commit_cache = None
+
+  def __init__(self, project, branch, base):
+    self.project = project
+    self.branch = branch
+    self.base = base
+
+  @property
+  def name(self):
+    return self.branch.name
+
+  @property
+  def commits(self):
+    if self._commit_cache is None:
+      self._commit_cache = self.project.bare_git.rev_list(
+        '--abbrev=8',
+        '--abbrev-commit',
+        '--pretty=oneline',
+        '--reverse',
+        '--date-order',
+        not_rev(self.base),
+        R_HEADS + self.name,
+        '--')
+    return self._commit_cache
+
+  @property
+  def date(self):
+    return self.project.bare_git.log(
+      '--pretty=format:%cd',
+      '-n', '1',
+      R_HEADS + self.name,
+      '--')
+
+  def UploadForReview(self):
+    self.project.UploadForReview(self.name)
+
+  @property
+  def tip_url(self):
+    me = self.project.GetBranch(self.name)
+    commit = self.project.bare_git.rev_parse(R_HEADS + self.name)
+    return 'http://%s/r/%s' % (me.remote.review, commit[0:12])
+
+
+class StatusColoring(Coloring):
+  def __init__(self, config):
+    Coloring.__init__(self, config, 'status')
+    self.project   = self.printer('header',    attr = 'bold')
+    self.branch    = self.printer('header',    attr = 'bold')
+    self.nobranch  = self.printer('nobranch',  fg = 'red')
+
+    self.added     = self.printer('added',     fg = 'green')
+    self.changed   = self.printer('changed',   fg = 'red')
+    self.untracked = self.printer('untracked', fg = 'red')
+
+
+class DiffColoring(Coloring):
+  def __init__(self, config):
+    Coloring.__init__(self, config, 'diff')
+    self.project   = self.printer('header',    attr = 'bold')
+
+
+class _CopyFile:
+  def __init__(self, src, dest):
+    self.src = src
+    self.dest = dest
+
+  def _Copy(self):
+    src = self.src
+    dest = self.dest
+    # copy file if it does not exist or is out of date
+    if not os.path.exists(dest) or not filecmp.cmp(src, dest):
+      try:
+        # remove existing file first, since it might be read-only
+        if os.path.exists(dest):
+          os.remove(dest)
+        shutil.copy(src, dest)
+        # make the file read-only
+        mode = os.stat(dest)[stat.ST_MODE]
+        mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
+        os.chmod(dest, mode)
+      except IOError:
+           print >>sys.stderr, \
+              'error: Cannot copy file %s to %s' \
+              % (src, dest)
+
+
+class Project(object):
+  def __init__(self,
+               manifest,
+               name,
+               remote,
+               gitdir,
+               worktree,
+               relpath,
+               revision):
+    self.manifest = manifest
+    self.name = name
+    self.remote = remote
+    self.gitdir = gitdir
+    self.worktree = worktree
+    self.relpath = relpath
+    self.revision = revision
+    self.snapshots = {}
+    self.extraRemotes = {}
+    self.copyfiles = []
+    self.config = GitConfig.ForRepository(
+                    gitdir = self.gitdir,
+                    defaults =  self.manifest.globalConfig)
+
+    self.work_git = self._GitGetByExec(self, bare=False)
+    self.bare_git = self._GitGetByExec(self, bare=True)
+
+  @property
+  def Exists(self):
+    return os.path.isdir(self.gitdir)
+
+  @property
+  def CurrentBranch(self):
+    """Obtain the name of the currently checked out branch.
+       The branch name omits the 'refs/heads/' prefix.
+       None is returned if the project is on a detached HEAD.
+    """
+    try:
+      b = self.work_git.GetHead()
+    except GitError:
+      return None
+    if b.startswith(R_HEADS):
+      return b[len(R_HEADS):]
+    return None
+
+  def IsDirty(self, consider_untracked=True):
+    """Is the working directory modified in some way?
+    """
+    self.work_git.update_index('-q',
+                               '--unmerged',
+                               '--ignore-missing',
+                               '--refresh')
+    if self.work_git.DiffZ('diff-index','-M','--cached',HEAD):
+      return True
+    if self.work_git.DiffZ('diff-files'):
+      return True
+    if consider_untracked and self.work_git.LsOthers():
+      return True
+    return False
+
+  _userident_name = None
+  _userident_email = None
+
+  @property
+  def UserName(self):
+    """Obtain the user's personal name.
+    """
+    if self._userident_name is None:
+      self._LoadUserIdentity()
+    return self._userident_name
+
+  @property
+  def UserEmail(self):
+    """Obtain the user's email address.  This is very likely
+       to be their Gerrit login.
+    """
+    if self._userident_email is None:
+      self._LoadUserIdentity()
+    return self._userident_email
+
+  def _LoadUserIdentity(self):
+      u = self.bare_git.var('GIT_COMMITTER_IDENT')
+      m = re.compile("^(.*) <([^>]*)> ").match(u)
+      if m:
+        self._userident_name = m.group(1)
+        self._userident_email = m.group(2)
+      else:
+        self._userident_name = ''
+        self._userident_email = ''
+
+  def GetRemote(self, name):
+    """Get the configuration for a single remote.
+    """
+    return self.config.GetRemote(name)
+
+  def GetBranch(self, name):
+    """Get the configuration for a single branch.
+    """
+    return self.config.GetBranch(name)
+
+
+## Status Display ##
+
+  def PrintWorkTreeStatus(self):
+    """Prints the status of the repository to stdout.
+    """
+    if not os.path.isdir(self.worktree):
+      print ''
+      print 'project %s/' % self.relpath
+      print '  missing (run "repo sync")'
+      return
+
+    self.work_git.update_index('-q',
+                               '--unmerged',
+                               '--ignore-missing',
+                               '--refresh')
+    di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD)
+    df = self.work_git.DiffZ('diff-files')
+    do = self.work_git.LsOthers()
+    if not di and not df and not do:
+      return
+
+    out = StatusColoring(self.config)
+    out.project('project %-40s', self.relpath + '/')
+
+    branch = self.CurrentBranch
+    if branch is None:
+      out.nobranch('(*** NO BRANCH ***)')
+    else:
+      out.branch('branch %s', branch)
+    out.nl()
+
+    paths = list()
+    paths.extend(di.keys())
+    paths.extend(df.keys())
+    paths.extend(do)
+
+    paths = list(set(paths))
+    paths.sort()
+
+    for p in paths:
+      try: i = di[p]
+      except KeyError: i = None
+
+      try: f = df[p]
+      except KeyError: f = None
+ 
+      if i: i_status = i.status.upper()
+      else: i_status = '-'
+
+      if f: f_status = f.status.lower()
+      else: f_status = '-'
+
+      if i and i.src_path:
+        line = ' %s%s\t%s => (%s%%)' % (i_status, f_status,
+                                        i.src_path, p, i.level)
+      else:
+        line = ' %s%s\t%s' % (i_status, f_status, p)
+
+      if i and not f:
+        out.added('%s', line)
+      elif (i and f) or (not i and f):
+        out.changed('%s', line)
+      elif not i and not f:
+        out.untracked('%s', line)
+      else:
+        out.write('%s', line)
+      out.nl()
+
+  def PrintWorkTreeDiff(self):
+    """Prints the status of the repository to stdout.
+    """
+    out = DiffColoring(self.config)
+    cmd = ['diff']
+    if out.is_on:
+      cmd.append('--color')
+    cmd.append(HEAD)
+    cmd.append('--')
+    p = GitCommand(self,
+                   cmd,
+                   capture_stdout = True,
+                   capture_stderr = True)
+    has_diff = False
+    for line in p.process.stdout:
+      if not has_diff:
+        out.nl()
+        out.project('project %s/' % self.relpath)
+        out.nl()
+        has_diff = True
+      print line[:-1]
+    p.Wait()
+
+
+## Publish / Upload ##
+
+  def WasPublished(self, branch):
+    """Was the branch published (uploaded) for code review?
+       If so, returns the SHA-1 hash of the last published
+       state for the branch.
+    """
+    try:
+      return self.bare_git.rev_parse(R_PUB + branch)
+    except GitError:
+      return None
+
+  def CleanPublishedCache(self):
+    """Prunes any stale published refs.
+    """
+    heads = set()
+    canrm = {}
+    for name, id in self._allrefs.iteritems():
+      if name.startswith(R_HEADS):
+        heads.add(name)
+      elif name.startswith(R_PUB):
+        canrm[name] = id
+
+    for name, id in canrm.iteritems():
+      n = name[len(R_PUB):]
+      if R_HEADS + n not in heads:
+        self.bare_git.DeleteRef(name, id)
+
+  def GetUploadableBranches(self):
+    """List any branches which can be uploaded for review.
+    """
+    heads = {}
+    pubed = {}
+
+    for name, id in self._allrefs.iteritems():
+      if name.startswith(R_HEADS):
+        heads[name[len(R_HEADS):]] = id
+      elif name.startswith(R_PUB):
+        pubed[name[len(R_PUB):]] = id
+
+    ready = []
+    for branch, id in heads.iteritems():
+      if branch in pubed and pubed[branch] == id:
+        continue
+
+      branch = self.GetBranch(branch)
+      base = branch.LocalMerge
+      if branch.LocalMerge:
+        rb = ReviewableBranch(self, branch, base)
+        if rb.commits:
+          ready.append(rb)
+    return ready
+
+  def UploadForReview(self, branch=None):
+    """Uploads the named branch for code review.
+    """
+    if branch is None:
+      branch = self.CurrentBranch
+    if branch is None:
+      raise GitError('not currently on a branch')
+
+    branch = self.GetBranch(branch)
+    if not branch.LocalMerge:
+      raise GitError('branch %s does not track a remote' % branch.name)
+    if not branch.remote.review:
+      raise GitError('remote %s has no review url' % branch.remote.name)
+
+    dest_branch = branch.merge
+    if not dest_branch.startswith(R_HEADS):
+      dest_branch = R_HEADS + dest_branch
+
+    base_list = []
+    for name, id in self._allrefs.iteritems():
+      if branch.remote.WritesTo(name):
+        base_list.append(not_rev(name))
+    if not base_list:
+      raise GitError('no base refs, cannot upload %s' % branch.name)
+
+    print >>sys.stderr, ''
+    _info("Uploading %s to %s:", branch.name, self.name)
+    try:
+      UploadBundle(project = self,
+                   server = branch.remote.review,
+                   email = self.UserEmail,
+                   dest_project = self.name,
+                   dest_branch = dest_branch,
+                   src_branch = R_HEADS + branch.name,
+                   bases = base_list)
+    except proto_client.ClientLoginError:
+      raise UploadError('Login failure')
+    except urllib2.HTTPError, e:
+      raise UploadError('HTTP error %d' % e.code)
+
+    msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
+    self.bare_git.UpdateRef(R_PUB + branch.name,
+                            R_HEADS + branch.name,
+                            message = msg)
+
+
+## Sync ##
+
+  def Sync_NetworkHalf(self):
+    """Perform only the network IO portion of the sync process.
+       Local working directory/branch state is not affected.
+    """
+    if not self.Exists:
+      print >>sys.stderr
+      print >>sys.stderr, 'Initializing project %s ...' % self.name
+      self._InitGitDir()
+    self._InitRemote()
+    for r in self.extraRemotes.values():
+      if not self._RemoteFetch(r.name):
+        return False
+    if not self._SnapshotDownload():
+      return False
+    if not self._RemoteFetch():
+      return False
+    self._InitMRef()
+    return True
+    
+  def _CopyFiles(self):
+    for file in self.copyfiles:
+      file._Copy()
+
+  def Sync_LocalHalf(self):
+    """Perform only the local IO portion of the sync process.
+       Network access is not required.
+
+       Return:
+         True:  the sync was successful
+         False: the sync requires user input
+    """
+    self._InitWorkTree()
+    self.CleanPublishedCache()
+
+    rem = self.GetRemote(self.remote.name)
+    rev = rem.ToLocal(self.revision)
+    branch = self.CurrentBranch
+
+    if branch is None:
+      # Currently on a detached HEAD.  The user is assumed to
+      # not have any local modifications worth worrying about.
+      #
+      lost = self._revlist(not_rev(rev), HEAD)
+      if lost:
+        _info("[%s] Discarding %d commits", self.name, len(lost))
+      try:
+        self._Checkout(rev, quiet=True)
+      except GitError:
+        return False
+      self._CopyFiles()
+      return True
+
+    branch = self.GetBranch(branch)
+    merge = branch.LocalMerge
+
+    if not merge:
+      # The current branch has no tracking configuration.
+      # Jump off it to a deatched HEAD.
+      #
+      _info("[%s] Leaving %s"
+            " (does not track any upstream)",
+            self.name,
+            branch.name)
+      try:
+        self._Checkout(rev, quiet=True)
+      except GitError:
+        return False
+      self._CopyFiles()
+      return True
+
+    upstream_gain = self._revlist(not_rev(HEAD), rev)
+    pub = self.WasPublished(branch.name)
+    if pub:
+      not_merged = self._revlist(not_rev(rev), pub)
+      if not_merged:
+        if upstream_gain:
+          # The user has published this branch and some of those
+          # commits are not yet merged upstream.  We do not want
+          # to rewrite the published commits so we punt.
+          #
+          _info("[%s] Branch %s is published,"
+                " but is now %d commits behind.",
+                self.name, branch.name, len(upstream_gain))
+          _info("[%s] Consider merging or rebasing the"
+                " unpublished commits.", self.name)
+        return True
+
+    if merge == rev:
+      try:
+        old_merge = self.bare_git.rev_parse('%s@{1}' % merge)
+      except GitError:
+        old_merge = merge
+    else:
+      # The upstream switched on us.  Time to cross our fingers
+      # and pray that the old upstream also wasn't in the habit
+      # of rebasing itself.
+      #
+      _info("[%s] Manifest switched from %s to %s",
+            self.name, merge, rev)
+      old_merge = merge
+
+    if rev == old_merge:
+      upstream_lost = []
+    else:
+      upstream_lost = self._revlist(not_rev(rev), old_merge)
+
+    if not upstream_lost and not upstream_gain:
+      # Trivially no changes caused by the upstream.
+      #
+      return True
+
+    if self.IsDirty(consider_untracked=False):
+      _warn('[%s] commit (or discard) uncommitted changes'
+            ' before sync', self.name)
+      return False
+
+    if upstream_lost:
+      # Upstream rebased.  Not everything in HEAD
+      # may have been caused by the user.
+      #
+      _info("[%s] Discarding %d commits removed from upstream",
+            self.name, len(upstream_lost))
+
+    branch.remote = rem
+    branch.merge = self.revision
+    branch.Save()
+
+    my_changes = self._revlist(not_rev(old_merge), HEAD)
+    if my_changes:
+      try:
+        self._Rebase(upstream = old_merge, onto = rev)
+      except GitError:
+        return False
+    elif upstream_lost:
+      try:
+        self._ResetHard(rev)
+      except GitError:
+        return False
+    else:
+      try:
+        self._FastForward(rev)
+      except GitError:
+        return False
+
+    self._CopyFiles()
+    return True
+
+  def _SnapshotDownload(self):
+    if self.snapshots:
+      have = set(self._allrefs.keys())
+      need = []
+
+      for tag, sn in self.snapshots.iteritems():
+        if tag not in have:
+          need.append(sn)
+
+      if need:
+        print >>sys.stderr, """
+  ***   Downloading source(s) from a mirror site.     ***
+  ***   If the network hangs, kill and restart repo.  ***
+"""
+        for sn in need:
+          try:
+            sn.Import()
+          except ImportError, e:
+            print >>sys.stderr, \
+              'error: Cannot import %s: %s' \
+              % (self.name, e)
+            return False
+        cmd = ['repack', '-a', '-d', '-f', '-l']
+        if GitCommand(self, cmd, bare = True).Wait() != 0:
+          return False
+    return True
+
+  def AddCopyFile(self, src, dest):
+    # dest should already be an absolute path, but src is project relative
+    # make src an absolute path
+    src = os.path.join(self.worktree, src)
+    self.copyfiles.append(_CopyFile(src, dest))
+
+
+## Branch Management ##
+
+  def StartBranch(self, name):
+    """Create a new branch off the manifest's revision.
+    """
+    branch = self.GetBranch(name)
+    branch.remote = self.GetRemote(self.remote.name)
+    branch.merge = self.revision
+
+    rev = branch.LocalMerge
+    cmd = ['checkout', '-b', branch.name, rev]
+    if GitCommand(self, cmd).Wait() == 0:
+      branch.Save()
+    else:
+      raise GitError('%s checkout %s ' % (self.name, rev))
+
+  def PruneHeads(self):
+    """Prune any topic branches already merged into upstream.
+    """
+    cb = self.CurrentBranch
+    kill = []
+    for name in self._allrefs.keys():
+      if name.startswith(R_HEADS):
+        name = name[len(R_HEADS):]
+        if cb is None or name != cb:
+          kill.append(name)
+
+    rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
+    if cb is not None \
+       and not self._revlist(HEAD + '...' + rev) \
+       and not self.IsDirty(consider_untracked = False):
+      self.work_git.DetachHead(HEAD)
+      kill.append(cb)
+
+    deleted = set()
+    if kill:
+      try:
+        old = self.bare_git.GetHead()
+      except GitError:
+        old = 'refs/heads/please_never_use_this_as_a_branch_name'
+
+      rm_re = re.compile(r"^Deleted branch (.*)\.$")
+      try:
+        self.bare_git.DetachHead(rev)
+
+        b = ['branch', '-d']
+        b.extend(kill)
+        b = GitCommand(self, b, bare=True,
+                       capture_stdout=True,
+                       capture_stderr=True)
+        b.Wait()
+      finally:
+        self.bare_git.SetHead(old)
+
+      for line in b.stdout.split("\n"):
+        m = rm_re.match(line)
+        if m:
+          deleted.add(m.group(1))
+
+      if deleted:
+        self.CleanPublishedCache()
+
+    if cb and cb not in kill:
+      kill.append(cb)
+      kill.sort()
+
+    kept = []
+    for branch in kill:
+      if branch not in deleted:
+        branch = self.GetBranch(branch)
+        base = branch.LocalMerge
+        if not base:
+          base = rev
+        kept.append(ReviewableBranch(self, branch, base))
+    return kept
+
+
+## Direct Git Commands ##
+
+  def _RemoteFetch(self, name=None):
+    if not name:
+      name = self.remote.name
+
+    hide_errors = False
+    if self.extraRemotes or self.snapshots:
+      hide_errors = True
+
+    proc = GitCommand(self,
+                      ['fetch', name],
+                      bare = True,
+                      capture_stderr = hide_errors)
+    if hide_errors:
+      err = proc.process.stderr.fileno()
+      buf = ''
+      while True:
+        b = os.read(err, 256)
+        if b:
+          buf += b
+        while buf:
+          r = buf.find('remote: error: unable to find ')
+          if r >= 0:
+            lf = buf.find('\n')
+            if lf < 0:
+              break
+            buf = buf[lf + 1:]
+            continue
+
+          cr = buf.find('\r')
+          if cr < 0:
+            break
+          os.write(2, buf[0:cr + 1])
+          buf = buf[cr + 1:]
+        if not b:
+          if buf:
+            os.write(2, buf)
+          break
+    return proc.Wait() == 0
+
+  def _Checkout(self, rev, quiet=False):
+    cmd = ['checkout']
+    if quiet:
+      cmd.append('-q')
+    cmd.append(rev)
+    cmd.append('--')
+    if GitCommand(self, cmd).Wait() != 0:
+      if self._allrefs:
+        raise GitError('%s checkout %s ' % (self.name, rev))
+
+  def _ResetHard(self, rev, quiet=True):
+    cmd = ['reset', '--hard']
+    if quiet:
+      cmd.append('-q')
+    cmd.append(rev)
+    if GitCommand(self, cmd).Wait() != 0:
+      raise GitError('%s reset --hard %s ' % (self.name, rev))
+
+  def _Rebase(self, upstream, onto = None):
+    cmd = ['rebase', '-i']
+    if onto is not None:
+      cmd.extend(['--onto', onto])
+    cmd.append(upstream)
+    if GitCommand(self, cmd, disable_editor=True).Wait() != 0:
+      raise GitError('%s rebase %s ' % (self.name, upstream))
+
+  def _FastForward(self, head):
+    cmd = ['merge', head]
+    if GitCommand(self, cmd).Wait() != 0:
+      raise GitError('%s merge %s ' % (self.name, head))
+
+  def _InitGitDir(self):
+    if not os.path.exists(self.gitdir):
+      os.makedirs(self.gitdir)
+      self.bare_git.init()
+      self.config.SetString('core.bare', None)
+
+      hooks = self._gitdir_path('hooks')
+      for old_hook in os.listdir(hooks):
+        os.remove(os.path.join(hooks, old_hook))
+
+      # TODO(sop) install custom repo hooks
+
+      m = self.manifest.manifestProject.config
+      for key in ['user.name', 'user.email']:
+        if m.Has(key, include_defaults = False):
+          self.config.SetString(key, m.GetString(key))
+
+  def _InitRemote(self):
+    if self.remote.fetchUrl:
+      remote = self.GetRemote(self.remote.name)
+
+      url = self.remote.fetchUrl
+      while url.endswith('/'):
+        url = url[:-1]
+      url += '/%s.git' % self.name
+      remote.url = url
+      remote.review = self.remote.reviewUrl
+
+      remote.ResetFetch()
+      remote.Save()
+
+    for r in self.extraRemotes.values():
+      remote = self.GetRemote(r.name)
+      remote.url = r.fetchUrl
+      remote.review = r.reviewUrl
+      remote.ResetFetch()
+      remote.Save()
+
+  def _InitMRef(self):
+    if self.manifest.branch:
+      msg = 'manifest set to %s' % self.revision
+      ref = R_M + self.manifest.branch
+
+      if IsId(self.revision):
+        dst = self.revision + '^0',
+        self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
+      else:
+        remote = self.GetRemote(self.remote.name)
+        dst = remote.ToLocal(self.revision)
+        self.bare_git.symbolic_ref('-m', msg, ref, dst)
+
+  def _InitWorkTree(self):
+    dotgit = os.path.join(self.worktree, '.git')
+    if not os.path.exists(dotgit):
+      os.makedirs(dotgit)
+
+      topdir = os.path.commonprefix([self.gitdir, dotgit])
+      if topdir.endswith('/'):
+        topdir = topdir[:-1]
+      else:
+        topdir = os.path.dirname(topdir)
+
+      tmpdir = dotgit
+      relgit = ''
+      while topdir != tmpdir:
+        relgit += '../'
+        tmpdir = os.path.dirname(tmpdir)
+      relgit += self.gitdir[len(topdir) + 1:]
+
+      for name in ['config',
+                   'description',
+                   'hooks',
+                   'info',
+                   'logs',
+                   'objects',
+                   'packed-refs',
+                   'refs',
+                   'rr-cache',
+                   'svn']:
+        os.symlink(os.path.join(relgit, name),
+                   os.path.join(dotgit, name))
+
+      rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
+      rev = self.bare_git.rev_parse('%s^0' % rev)
+
+      f = open(os.path.join(dotgit, HEAD), 'wb')
+      f.write("%s\n" % rev)
+      f.close()
+
+      cmd = ['read-tree', '--reset', '-u']
+      cmd.append('-v')
+      cmd.append('HEAD')
+      if GitCommand(self, cmd).Wait() != 0:
+        raise GitError("cannot initialize work tree")
+
+  def _gitdir_path(self, path):
+    return os.path.join(self.gitdir, path)
+
+  def _revlist(self, *args):
+    cmd = []
+    cmd.extend(args)
+    cmd.append('--')
+    return self.work_git.rev_list(*args)
+
+  @property
+  def _allrefs(self):
+    return self.bare_git.ListRefs()
+
+  class _GitGetByExec(object):
+    def __init__(self, project, bare):
+      self._project = project
+      self._bare = bare
+
+    def ListRefs(self, *args):
+      cmdv = ['for-each-ref', '--format=%(objectname) %(refname)']
+      cmdv.extend(args)
+      p = GitCommand(self._project,
+                     cmdv,
+                     bare = self._bare,
+                     capture_stdout = True,
+                     capture_stderr = True)
+      r = {}
+      for line in p.process.stdout:
+        id, name = line[:-1].split(' ', 2)
+        r[name] = id
+      if p.Wait() != 0:
+        raise GitError('%s for-each-ref %s: %s' % (
+                       self._project.name,
+                       str(args),
+                       p.stderr))
+      return r
+
+    def LsOthers(self):
+      p = GitCommand(self._project,
+                     ['ls-files',
+                      '-z',
+                      '--others',
+                      '--exclude-standard'],
+                     bare = False,
+                     capture_stdout = True,
+                     capture_stderr = True)
+      if p.Wait() == 0:
+        out = p.stdout
+        if out:
+          return out[:-1].split("\0")
+      return []
+
+    def DiffZ(self, name, *args):
+      cmd = [name]
+      cmd.append('-z')
+      cmd.extend(args)
+      p = GitCommand(self._project,
+                     cmd,
+                     bare = False,
+                     capture_stdout = True,
+                     capture_stderr = True)
+      try:
+        out = p.process.stdout.read()
+        r = {}
+        if out:
+          out = iter(out[:-1].split('\0'))
+          while out:
+            info = out.next()
+            path = out.next()
+
+            class _Info(object):
+              def __init__(self, path, omode, nmode, oid, nid, state):
+                self.path = path
+                self.src_path = None
+                self.old_mode = omode
+                self.new_mode = nmode
+                self.old_id = oid
+                self.new_id = nid
+
+                if len(state) == 1:
+                  self.status = state
+                  self.level = None
+                else:
+                  self.status = state[:1]
+                  self.level = state[1:]
+                  while self.level.startswith('0'):
+                    self.level = self.level[1:]
+
+            info = info[1:].split(' ')
+            info =_Info(path, *info)
+            if info.status in ('R', 'C'):
+              info.src_path = info.path
+              info.path = out.next()
+            r[info.path] = info
+        return r
+      finally:
+        p.Wait()
+
+    def GetHead(self):
+      return self.symbolic_ref(HEAD)
+
+    def SetHead(self, ref, message=None):
+      cmdv = []
+      if message is not None:
+        cmdv.extend(['-m', message])
+      cmdv.append(HEAD)
+      cmdv.append(ref)
+      self.symbolic_ref(*cmdv)
+
+    def DetachHead(self, new, message=None):
+      cmdv = ['--no-deref']
+      if message is not None:
+        cmdv.extend(['-m', message])
+      cmdv.append(HEAD)
+      cmdv.append(new)
+      self.update_ref(*cmdv)
+
+    def UpdateRef(self, name, new, old=None,
+                  message=None,
+                  detach=False):
+      cmdv = []
+      if message is not None:
+        cmdv.extend(['-m', message])
+      if detach:
+        cmdv.append('--no-deref')
+      cmdv.append(name)
+      cmdv.append(new)
+      if old is not None:
+        cmdv.append(old)
+      self.update_ref(*cmdv)
+
+    def DeleteRef(self, name, old=None):
+      if not old:
+        old = self.rev_parse(name)
+      self.update_ref('-d', name, old)
+
+    def rev_list(self, *args):
+      cmdv = ['rev-list']
+      cmdv.extend(args)
+      p = GitCommand(self._project,
+                     cmdv,
+                     bare = self._bare,
+                     capture_stdout = True,
+                     capture_stderr = True)
+      r = []
+      for line in p.process.stdout:
+        r.append(line[:-1])
+      if p.Wait() != 0:
+        raise GitError('%s rev-list %s: %s' % (
+                       self._project.name,
+                       str(args),
+                       p.stderr))
+      return r
+
+    def __getattr__(self, name):
+      name = name.replace('_', '-')
+      def runner(*args):
+        cmdv = [name]
+        cmdv.extend(args)
+        p = GitCommand(self._project,
+                       cmdv,
+                       bare = self._bare,
+                       capture_stdout = True,
+                       capture_stderr = True)
+        if p.Wait() != 0:
+          raise GitError('%s %s: %s' % (
+                         self._project.name,
+                         name,
+                         p.stderr))
+        r = p.stdout
+        if r.endswith('\n') and r.index('\n') == len(r) - 1:
+          return r[:-1]
+        return r
+      return runner
+
+
+class MetaProject(Project):
+  """A special project housed under .repo.
+  """
+  def __init__(self, manifest, name, gitdir, worktree):
+    repodir = manifest.repodir
+    Project.__init__(self,
+                     manifest = manifest,
+                     name = name,
+                     gitdir = gitdir,
+                     worktree = worktree,
+                     remote = Remote('origin'),
+                     relpath = '.repo/%s' % name,
+                     revision = 'refs/heads/master')
+
+  def PreSync(self):
+    if self.Exists:
+      cb = self.CurrentBranch
+      if cb:
+        base = self.GetBranch(cb).merge
+        if base:
+          self.revision = base
+
+  @property
+  def HasChanges(self):
+    """Has the remote received new commits not yet checked out?
+    """
+    rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
+    if self._revlist(not_rev(HEAD), rev):
+      return True
+    return False
diff --git a/remote.py b/remote.py
new file mode 100644
index 0000000..27a8f7a
--- /dev/null
+++ b/remote.py
@@ -0,0 +1,21 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Remote(object):
+  def __init__(self, name, fetch=None, review=None):
+    self.name = name
+    self.fetchUrl = fetch
+    self.reviewUrl = review
+    self.requiredCommits = []
diff --git a/repo b/repo
new file mode 100755
index 0000000..d5f69fb
--- /dev/null
+++ b/repo
@@ -0,0 +1,587 @@
+#!/bin/sh
+
+## repo default configuration
+##
+REPO_URL='git://android.kernel.org/tools/repo.git'
+REPO_REV='stable'
+
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+magic='--calling-python-from-/bin/sh--'
+"""exec" python2.4 -E "$0" "$@" """#$magic"
+if __name__ == '__main__':
+  import sys
+  if sys.argv[-1] == '#%s' % magic:
+    del sys.argv[-1]
+del magic
+
+# increment this whenever we make important changes to this script
+VERSION = (1, 4)
+
+# increment this if the MAINTAINER_KEYS block is modified
+KEYRING_VERSION = (1,0)
+MAINTAINER_KEYS = """
+
+     Repo Maintainer <repo@android.kernel.org>
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.2.2 (GNU/Linux)
+
+mQGiBEj3ugERBACrLJh/ZPyVSKeClMuznFIrsQ+hpNnmJGw1a9GXKYKk8qHPhAZf
+WKtrBqAVMNRLhL85oSlekRz98u41H5si5zcuv+IXJDF5MJYcB8f22wAy15lUqPWi
+VCkk1l8qqLiuW0fo+ZkPY5qOgrvc0HW1SmdH649uNwqCbcKb6CxaTxzhOwCgj3AP
+xI1WfzLqdJjsm1Nq98L0cLcD/iNsILCuw44PRds3J75YP0pze7YF/6WFMB6QSFGu
+aUX1FsTTztKNXGms8i5b2l1B8JaLRWq/jOnZzyl1zrUJhkc0JgyZW5oNLGyWGhKD
+Fxp5YpHuIuMImopWEMFIRQNrvlg+YVK8t3FpdI1RY0LYqha8pPzANhEYgSfoVzOb
+fbfbA/4ioOrxy8ifSoga7ITyZMA+XbW8bx33WXutO9N7SPKS/AK2JpasSEVLZcON
+ae5hvAEGVXKxVPDjJBmIc2cOe7kOKSi3OxLzBqrjS2rnjiP4o0ekhZIe4+ocwVOg
+e0PLlH5avCqihGRhpoqDRsmpzSHzJIxtoeb+GgGEX8KkUsVAhbQpUmVwbyBNYWlu
+dGFpbmVyIDxyZXBvQGFuZHJvaWQua2VybmVsLm9yZz6IYAQTEQIAIAUCSPe6AQIb
+AwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEBZTDV6SD1xl1GEAn0x/OKQpy7qI
+6G73NJviU0IUMtftAKCFMUhGb/0bZvQ8Rm3QCUpWHyEIu7kEDQRI97ogEBAA2wI6
+5fs9y/rMwD6dkD/vK9v4C9mOn1IL5JCPYMJBVSci+9ED4ChzYvfq7wOcj9qIvaE0
+GwCt2ar7Q56me5J+byhSb32Rqsw/r3Vo5cZMH80N4cjesGuSXOGyEWTe4HYoxnHv
+gF4EKI2LK7xfTUcxMtlyn52sUpkfKsCpUhFvdmbAiJE+jCkQZr1Z8u2KphV79Ou+
+P1N5IXY/XWOlq48Qf4MWCYlJFrB07xjUjLKMPDNDnm58L5byDrP/eHysKexpbakL
+xCmYyfT6DV1SWLblpd2hie0sL3YejdtuBMYMS2rI7Yxb8kGuqkz+9l1qhwJtei94
+5MaretDy/d/JH/pRYkRf7L+ke7dpzrP+aJmcz9P1e6gq4NJsWejaALVASBiioqNf
+QmtqSVzF1wkR5avZkFHuYvj6V/t1RrOZTXxkSk18KFMJRBZrdHFCWbc5qrVxUB6e
+N5pja0NFIUCigLBV1c6I2DwiuboMNh18VtJJh+nwWeez/RueN4ig59gRTtkcc0PR
+35tX2DR8+xCCFVW/NcJ4PSePYzCuuLvp1vEDHnj41R52Fz51hgddT4rBsp0nL+5I
+socSOIIezw8T9vVzMY4ArCKFAVu2IVyBcahTfBS8q5EM63mONU6UVJEozfGljiMw
+xuQ7JwKcw0AUEKTKG7aBgBaTAgT8TOevpvlw91cAAwUP/jRkyVi/0WAb0qlEaq/S
+ouWxX1faR+vU3b+Y2/DGjtXQMzG0qpetaTHC/AxxHpgt/dCkWI6ljYDnxgPLwG0a
+Oasm94BjZc6vZwf1opFZUKsjOAAxRxNZyjUJKe4UZVuMTk6zo27Nt3LMnc0FO47v
+FcOjRyquvgNOS818irVHUf12waDx8gszKxQTTtFxU5/ePB2jZmhP6oXSe4K/LG5T
++WBRPDrHiGPhCzJRzm9BP0lTnGCAj3o9W90STZa65RK7IaYpC8TB35JTBEbrrNCp
+w6lzd74LnNEp5eMlKDnXzUAgAH0yzCQeMl7t33QCdYx2hRs2wtTQSjGfAiNmj/WW
+Vl5Jn+2jCDnRLenKHwVRFsBX2e0BiRWt/i9Y8fjorLCXVj4z+7yW6DawdLkJorEo
+p3v5ILwfC7hVx4jHSnOgZ65L9s8EQdVr1ckN9243yta7rNgwfcqb60ILMFF1BRk/
+0V7wCL+68UwwiQDvyMOQuqkysKLSDCLb7BFcyA7j6KG+5hpsREstFX2wK1yKeraz
+5xGrFy8tfAaeBMIQ17gvFSp/suc9DYO0ICK2BISzq+F+ZiAKsjMYOBNdH/h0zobQ
+HTHs37+/QLMomGEGKZMWi0dShU2J5mNRQu3Hhxl3hHDVbt5CeJBb26aQcQrFz69W
+zE3GNvmJosh6leayjtI9P2A6iEkEGBECAAkFAkj3uiACGwwACgkQFlMNXpIPXGWp
+TACbBS+Up3RpfYVfd63c1cDdlru13pQAn3NQy/SN858MkxN+zym86UBgOad2
+=CMiZ
+-----END PGP PUBLIC KEY BLOCK-----
+"""
+
+GIT = 'git'                     # our git command
+MIN_GIT_VERSION = (1, 5, 4)     # minimum supported git version
+repodir = '.repo'               # name of repo's private directory
+S_repo = 'repo'                 # special repo reposiory
+S_manifests = 'manifests'       # special manifest repository
+REPO_MAIN = S_repo + '/main.py' # main script
+
+
+import optparse
+import os
+import re
+import readline
+import subprocess
+import sys
+
+home_dot_repo = os.path.expanduser('~/.repoconfig')
+gpg_dir = os.path.join(home_dot_repo, 'gnupg')
+
+extra_args = []
+init_optparse = optparse.OptionParser(usage="repo init -u url [options]")
+
+# Logging
+group = init_optparse.add_option_group('Logging options')
+group.add_option('-q', '--quiet',
+                 dest="quiet", action="store_true", default=False,
+                 help="be quiet")
+
+# Manifest
+group = init_optparse.add_option_group('Manifest options')
+group.add_option('-u', '--manifest-url',
+                 dest='manifest_url',
+                 help='manifest repository location', metavar='URL')
+group.add_option('-b', '--manifest-branch',
+                 dest='manifest_branch',
+                 help='manifest branch or revision', metavar='REVISION')
+group.add_option('-m', '--manifest-name',
+                 dest='manifest_name',
+                 help='initial manifest file', metavar='NAME.xml')
+
+# Tool
+group = init_optparse.add_option_group('Version options')
+group.add_option('--repo-url',
+                 dest='repo_url',
+                 help='repo repository location', metavar='URL')
+group.add_option('--repo-branch',
+                 dest='repo_branch',
+                 help='repo branch or revision', metavar='REVISION')
+group.add_option('--no-repo-verify',
+                 dest='no_repo_verify', action='store_true',
+                 help='do not verify repo source code')
+
+
+class CloneFailure(Exception):
+  """Indicate the remote clone of repo itself failed.
+  """
+
+
+def _Init(args):
+  """Installs repo by cloning it over the network.
+  """
+  opt, args = init_optparse.parse_args(args)
+  if args or not opt.manifest_url:
+    init_optparse.print_usage()
+    sys.exit(1)
+
+  url = opt.repo_url
+  if not url:
+    url = REPO_URL
+    extra_args.append('--repo-url=%s' % url)
+
+  branch = opt.repo_branch
+  if not branch:
+    branch = REPO_REV
+    extra_args.append('--repo-branch=%s' % branch)
+
+  if branch.startswith('refs/heads/'):
+    branch = branch[len('refs/heads/'):]
+  if branch.startswith('refs/'):
+    print >>sys.stderr, "fatal: invalid branch name '%s'" % branch
+    raise CloneFailure()
+
+  if not os.path.isdir(repodir):
+    try:
+      os.mkdir(repodir)
+    except OSError, e:
+      print >>sys.stderr, \
+            'fatal: cannot make %s directory: %s' % (
+            repodir, e.strerror)
+      # Don't faise CloneFailure; that would delete the
+      # name. Instead exit immediately.
+      #
+      sys.exit(1)
+
+  _CheckGitVersion()
+  try:
+    if _NeedSetupGnuPG():
+      can_verify = _SetupGnuPG(opt.quiet)
+    else:
+      can_verify = True
+
+    if not opt.quiet:
+      print >>sys.stderr, 'Getting repo ...'
+      print >>sys.stderr, '   from %s' % url
+
+    dst = os.path.abspath(os.path.join(repodir, S_repo))
+    _Clone(url, dst, opt.quiet)
+
+    if can_verify and not opt.no_repo_verify:
+      rev = _Verify(dst, branch, opt.quiet)
+    else:
+      rev = 'refs/remotes/origin/%s^0' % branch
+
+    _Checkout(dst, branch, rev, opt.quiet)
+  except CloneFailure:
+    if opt.quiet:
+      print >>sys.stderr, \
+        'fatal: repo init failed; run without --quiet to see why'
+    raise
+
+
+def _CheckGitVersion():
+  cmd = [GIT, '--version']
+  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+  ver_str = proc.stdout.read().strip()
+  proc.stdout.close()
+
+  if not ver_str.startswith('git version '):
+    print >>sys.stderr, 'error: "%s" unsupported' % ver_str
+    raise CloneFailure()
+
+  ver_str = ver_str[len('git version '):].strip()
+  ver_act = tuple(map(lambda x: int(x), ver_str.split('.')[0:3]))
+  if ver_act < MIN_GIT_VERSION:
+    need = '.'.join(map(lambda x: str(x), MIN_GIT_VERSION))
+    print >>sys.stderr, 'fatal: git %s or later required' % need
+    raise CloneFailure()
+
+
+def _NeedSetupGnuPG():
+  if not os.path.isdir(home_dot_repo):
+    return True
+
+  kv = os.path.join(home_dot_repo, 'keyring-version')
+  if not os.path.exists(kv):
+    return True
+
+  kv = open(kv).read()
+  if not kv:
+    return True
+
+  kv = tuple(map(lambda x: int(x), kv.split('.')))
+  if kv < KEYRING_VERSION:
+    return True
+  return False
+
+
+def _SetupGnuPG(quiet):
+  if not os.path.isdir(home_dot_repo):
+    try:
+      os.mkdir(home_dot_repo)
+    except OSError, e:
+      print >>sys.stderr, \
+            'fatal: cannot make %s directory: %s' % (
+            home_dot_repo, e.strerror)
+      sys.exit(1)
+
+  if not os.path.isdir(gpg_dir):
+    try:
+      os.mkdir(gpg_dir, 0700)
+    except OSError, e:
+      print >>sys.stderr, \
+            'fatal: cannot make %s directory: %s' % (
+            gpg_dir, e.strerror)
+      sys.exit(1)
+
+  env = dict(os.environ)
+  env['GNUPGHOME'] = gpg_dir
+
+  cmd = ['gpg', '--import']
+  try:
+    proc = subprocess.Popen(cmd,
+                            env = env,
+                            stdin = subprocess.PIPE)
+  except OSError, e:
+    if not quiet:
+      print >>sys.stderr, 'warning: gpg (GnuPG) is not available.'
+      print >>sys.stderr, 'warning: Installing it is strongly encouraged.'
+      print >>sys.stderr
+    return False
+
+  proc.stdin.write(MAINTAINER_KEYS)
+  proc.stdin.close()
+
+  if proc.wait() != 0:
+    print >>sys.stderr, 'fatal: registering repo maintainer keys failed'
+    sys.exit(1)
+  print
+
+  fd = open(os.path.join(home_dot_repo, 'keyring-version'), 'w')
+  fd.write('.'.join(map(lambda x: str(x), KEYRING_VERSION)) + '\n')
+  fd.close()
+  return True
+
+
+def _SetConfig(local, name, value):
+  """Set a git configuration option to the specified value.
+  """
+  cmd = [GIT, 'config', name, value]
+  if subprocess.Popen(cmd, cwd = local).wait() != 0:
+    raise CloneFailure()
+
+
+def _Fetch(local, quiet, *args):
+  cmd = [GIT, 'fetch']
+  if quiet:
+    cmd.append('--quiet')
+    err = subprocess.PIPE
+  else:
+    err = None
+  cmd.extend(args)
+  cmd.append('origin')
+
+  proc = subprocess.Popen(cmd, cwd = local, stderr = err)
+  if err:
+    proc.stderr.read()
+    proc.stderr.close()
+  if proc.wait() != 0:
+    raise CloneFailure()
+
+
+def _Clone(url, local, quiet):
+  """Clones a git repository to a new subdirectory of repodir
+  """
+  try:
+    os.mkdir(local)
+  except OSError, e:
+    print >>sys.stderr, \
+          'fatal: cannot make %s directory: %s' \
+          % (local, e.strerror)
+    raise CloneFailure()
+
+  cmd = [GIT, 'init', '--quiet']
+  try:
+    proc = subprocess.Popen(cmd, cwd = local)
+  except OSError, e:
+    print >>sys.stderr
+    print >>sys.stderr, "fatal: '%s' is not available" % GIT
+    print >>sys.stderr, 'fatal: %s' % e
+    print >>sys.stderr
+    print >>sys.stderr, 'Please make sure %s is installed'\
+                        ' and in your path.' % GIT
+    raise CloneFailure()
+  if proc.wait() != 0:
+    print >>sys.stderr, 'fatal: could not create %s' % local
+    raise CloneFailure()
+
+  _SetConfig(local, 'remote.origin.url', url)
+  _SetConfig(local, 'remote.origin.fetch',
+                    '+refs/heads/*:refs/remotes/origin/*')
+  _Fetch(local, quiet)
+  _Fetch(local, quiet, '--tags')
+
+
+def _Verify(cwd, branch, quiet):
+  """Verify the branch has been signed by a tag.
+  """
+  cmd = [GIT, 'describe', 'origin/%s' % branch]
+  proc = subprocess.Popen(cmd,
+                          stdout=subprocess.PIPE,
+                          stderr=subprocess.PIPE,
+                          cwd = cwd)
+  cur = proc.stdout.read().strip()
+  proc.stdout.close()
+
+  proc.stderr.read()
+  proc.stderr.close()
+
+  if proc.wait() != 0 or not cur:
+    print >>sys.stderr
+    print >>sys.stderr,\
+      "fatal: branch '%s' has not been signed" \
+      % branch
+    raise CloneFailure()
+
+  m = re.compile(r'^(.*)-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur)
+  if m:
+    cur = m.group(1)
+    if not quiet:
+      print >>sys.stderr
+      print >>sys.stderr, \
+        "info: Ignoring branch '%s'; using tagged release '%s'" \
+        % (branch, cur)
+      print >>sys.stderr
+
+  env = dict(os.environ)
+  env['GNUPGHOME'] = gpg_dir
+
+  cmd = [GIT, 'tag', '-v', cur]
+  proc = subprocess.Popen(cmd,
+                          stdout = subprocess.PIPE,
+                          stderr = subprocess.PIPE,
+                          cwd = cwd,
+                          env = env)
+  out = proc.stdout.read()
+  proc.stdout.close()
+
+  err = proc.stderr.read()
+  proc.stderr.close()
+
+  if proc.wait() != 0:
+    print >>sys.stderr
+    print >>sys.stderr, out
+    print >>sys.stderr, err
+    print >>sys.stderr
+    raise CloneFailure()
+  return '%s^0' % cur
+
+
+def _Checkout(cwd, branch, rev, quiet):
+  """Checkout an upstream branch into the repository and track it.
+  """
+  cmd = [GIT, 'update-ref', 'refs/heads/default', rev]
+  if subprocess.Popen(cmd, cwd = cwd).wait() != 0:
+    raise CloneFailure()
+
+  _SetConfig(cwd, 'branch.default.remote', 'origin')
+  _SetConfig(cwd, 'branch.default.merge', 'refs/heads/%s' % branch)
+
+  cmd = [GIT, 'symbolic-ref', 'HEAD', 'refs/heads/default']
+  if subprocess.Popen(cmd, cwd = cwd).wait() != 0:
+    raise CloneFailure()
+
+  cmd = [GIT, 'read-tree', '--reset', '-u']
+  if not quiet:
+    cmd.append('-v')
+  cmd.append('HEAD')
+  if subprocess.Popen(cmd, cwd = cwd).wait() != 0:
+    raise CloneFailure()
+
+
+def _FindRepo():
+  """Look for a repo installation, starting at the current directory.
+  """
+  dir = os.getcwd()
+  repo = None
+
+  while dir != '/' and not repo:
+    repo = os.path.join(dir, repodir, REPO_MAIN)
+    if not os.path.isfile(repo):
+      repo = None
+      dir = os.path.dirname(dir)
+  return (repo, os.path.join(dir, repodir))
+
+
+class _Options:
+  help = False
+
+
+def _ParseArguments(args):
+  cmd = None
+  opt = _Options()
+  arg = []
+
+  for i in xrange(0, len(args)):
+    a = args[i]
+    if a == '-h' or a == '--help':
+      opt.help = True
+
+    elif not a.startswith('-'):
+      cmd = a
+      arg = args[i + 1:]
+      break
+  return cmd, opt, arg
+
+
+def _Usage():
+  print >>sys.stderr,\
+"""usage: repo COMMAND [ARGS]
+
+repo is not yet installed.  Use "repo init" to install it here.
+
+The most commonly used repo commands are:
+
+  init      Install repo in the current working directory
+  help      Display detailed help on a command
+
+For access to the full online help, install repo ("repo init").
+"""
+  sys.exit(1)
+
+
+def _Help(args):
+  if args:
+    if args[0] == 'init':
+      init_optparse.print_help()
+    else:
+      print >>sys.stderr,\
+      "error: '%s' is not a bootstrap command.\n"\
+      '        For access to online help, install repo ("repo init").'\
+      % args[0]
+  else:
+    _Usage()
+  sys.exit(1)
+
+
+def _NotInstalled():
+  print >>sys.stderr,\
+'error: repo is not installed.  Use "repo init" to install it here.'
+  sys.exit(1)
+
+
+def _NoCommands(cmd):
+  print >>sys.stderr,\
+"""error: command '%s' requires repo to be installed first.
+       Use "repo init" to install it here.""" % cmd
+  sys.exit(1)
+
+
+def _RunSelf(wrapper_path):
+  my_dir = os.path.dirname(wrapper_path)
+  my_main = os.path.join(my_dir, 'main.py')
+  my_git = os.path.join(my_dir, '.git')
+
+  if os.path.isfile(my_main) and os.path.isdir(my_git):
+    for name in ['manifest.py',
+                 'project.py',
+                 'subcmds']:
+      if not os.path.exists(os.path.join(my_dir, name)):
+        return None, None
+    return my_main, my_git
+  return None, None
+
+
+def _SetDefaultsTo(gitdir):
+  global REPO_URL
+  global REPO_REV
+
+  REPO_URL = gitdir
+  proc = subprocess.Popen([GIT,
+                           '--git-dir=%s' % gitdir,
+                           'symbolic-ref',
+                           'HEAD'],
+                          stdout = subprocess.PIPE,
+                          stderr = subprocess.PIPE)
+  REPO_REV = proc.stdout.read().strip()
+  proc.stdout.close()
+
+  proc.stderr.read()
+  proc.stderr.close()
+
+  if proc.wait() != 0:
+    print >>sys.stderr, 'fatal: %s has no current branch' % gitdir
+    sys.exit(1)
+
+
+def main(orig_args):
+  main, dir = _FindRepo()
+  cmd, opt, args = _ParseArguments(orig_args)
+
+  wrapper_path = os.path.abspath(__file__)
+  my_main, my_git = _RunSelf(wrapper_path)
+
+  if not main:
+    if opt.help:
+      _Usage()
+    if cmd == 'help':
+      _Help(args)
+    if not cmd:
+      _NotInstalled()
+    if cmd == 'init':
+      if my_git:
+        _SetDefaultsTo(my_git)
+      try:
+        _Init(args)
+      except CloneFailure:
+        for root, dirs, files in os.walk(repodir, topdown=False):
+          for name in files:
+            os.remove(os.path.join(root, name))
+          for name in dirs:
+            os.rmdir(os.path.join(root, name))
+        os.rmdir(repodir)
+        sys.exit(1)
+      main, dir = _FindRepo()
+    else:
+      _NoCommands(cmd)
+
+  if my_main:
+    main = my_main
+
+  ver_str = '.'.join(map(lambda x: str(x), VERSION))
+  me = [main,
+        '--repo-dir=%s' % dir,
+        '--wrapper-version=%s' % ver_str,
+        '--wrapper-path=%s' % wrapper_path,
+        '--']
+  me.extend(orig_args)
+  me.extend(extra_args)
+  try:
+    os.execv(main, me)
+  except OSError, e:
+    print >>sys.stderr, "fatal: unable to start %s" % main
+    print >>sys.stderr, "fatal: %s" % e
+    sys.exit(148)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
+
diff --git a/subcmds/__init__.py b/subcmds/__init__.py
new file mode 100644
index 0000000..a2286e7
--- /dev/null
+++ b/subcmds/__init__.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+all = {}
+
+my_dir = os.path.dirname(__file__)
+for py in os.listdir(my_dir):
+  if py == '__init__.py':
+    continue
+
+  if py.endswith('.py'):
+    name = py[:-3]
+
+    clsn = name.capitalize()
+    while clsn.find('_') > 0:
+      h = clsn.index('_')
+      clsn = clsn[0:h] + clsn[h + 1:].capitalize()
+
+    mod = __import__(__name__,
+                     globals(),
+                     locals(),
+                     ['%s' % name])
+    mod = getattr(mod, name)
+    try:
+      cmd = getattr(mod, clsn)()
+    except AttributeError:
+      raise SyntaxError, '%s/%s does not define class %s' % (
+                         __name__, py, clsn)
+
+    name = name.replace('_', '-')
+    cmd.NAME = name
+    all[name] = cmd
+
+if 'help' in all:
+  all['help'].commands = all
diff --git a/subcmds/compute_snapshot_check.py b/subcmds/compute_snapshot_check.py
new file mode 100644
index 0000000..82db359
--- /dev/null
+++ b/subcmds/compute_snapshot_check.py
@@ -0,0 +1,169 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import tempfile
+
+from command import Command
+from error import GitError, NoSuchProjectError
+from git_config import IsId
+from import_tar import ImportTar
+from import_zip import ImportZip
+from project import Project
+from remote import Remote
+
+def _ToCommit(project, rev):
+  return project.bare_git.rev_parse('--verify', '%s^0' % rev)
+
+def _Missing(project, rev):
+  return project._revlist('--objects', rev, '--not', '--all')
+
+
+class ComputeSnapshotCheck(Command):
+  common = False
+  helpSummary = "Compute the check value for a new snapshot"
+  helpUsage = """
+%prog -p NAME -v VERSION -s FILE [options]
+"""
+  helpDescription = """
+%prog computes and then displays the proper check value for a
+snapshot, so it can be pasted into the manifest file for a project.
+"""
+
+  def _Options(self, p):
+    g = p.add_option_group('Snapshot description options')
+    g.add_option('-p', '--project',
+                 dest='project', metavar='NAME',
+                 help='destination project name')
+    g.add_option('-v', '--version',
+                 dest='version', metavar='VERSION',
+                 help='upstream version/revision identifier')
+    g.add_option('-s', '--snapshot',
+                 dest='snapshot', metavar='PATH',
+                 help='local tarball path')
+    g.add_option('--new-project',
+                 dest='new_project', action='store_true',
+                 help='destinition is a new project')
+    g.add_option('--keep',
+                 dest='keep_git', action='store_true',
+                 help='keep the temporary git repository')
+
+    g = p.add_option_group('Base revision grafting options')
+    g.add_option('--prior',
+                 dest='prior', metavar='COMMIT',
+                 help='prior revision checksum')
+
+    g = p.add_option_group('Path mangling options')
+    g.add_option('--strip-prefix',
+                 dest='strip_prefix', metavar='PREFIX',
+                 help='remove prefix from all paths on import')
+    g.add_option('--insert-prefix',
+                 dest='insert_prefix', metavar='PREFIX',
+                 help='insert prefix before all paths on import')
+
+
+  def _Compute(self, opt):
+    try:
+      real_project = self.GetProjects([opt.project])[0]
+    except NoSuchProjectError:
+      if opt.new_project:
+        print >>sys.stderr, \
+          "warning: project '%s' does not exist" % opt.project
+      else:
+        raise NoSuchProjectError(opt.project)
+
+    self._tmpdir = tempfile.mkdtemp()
+    project = Project(manifest = self.manifest,
+                      name = opt.project,
+                      remote = Remote('origin'),
+                      gitdir = os.path.join(self._tmpdir, '.git'),
+                      worktree = self._tmpdir,
+                      relpath = opt.project,
+                      revision = 'refs/heads/master')
+    project._InitGitDir()
+
+    url = 'file://%s' % os.path.abspath(opt.snapshot)
+
+    imp = None
+    for cls in [ImportTar, ImportZip]:
+      if cls.CanAccept(url):
+        imp = cls()
+        break
+    if not imp:
+      print >>sys.stderr, 'error: %s unsupported' % opt.snapshot
+      sys.exit(1)
+
+    imp.SetProject(project)
+    imp.SetVersion(opt.version)
+    imp.AddUrl(url)
+
+    if opt.prior:
+      if opt.new_project:
+        if not IsId(opt.prior):
+          print >>sys.stderr, 'error: --prior=%s not valid' % opt.prior
+          sys.exit(1)
+      else:
+        try:
+          opt.prior = _ToCommit(real_project, opt.prior)
+          missing = _Missing(real_project, opt.prior)
+        except GitError, e:
+          print >>sys.stderr,\
+            'error: --prior=%s not valid\n%s' \
+            % (opt.prior, e)
+          sys.exit(1)
+        if missing:
+          print >>sys.stderr,\
+            'error: --prior=%s is valid, but is not reachable' \
+            % opt.prior
+          sys.exit(1)
+      imp.SetParent(opt.prior)
+
+    src = opt.strip_prefix
+    dst = opt.insert_prefix
+    if src or dst:
+      if src is None:
+        src = ''
+      if dst is None:
+        dst = ''
+      imp.RemapPath(src, dst)
+    commitId = imp.Import()
+
+    print >>sys.stderr,"%s\t%s" % (commitId, imp.version)
+    return project
+
+  def Execute(self, opt, args):
+    if args \
+       or not opt.project \
+       or not opt.version \
+       or not opt.snapshot:
+      self.Usage()
+
+    success = False
+    project = None
+    try:
+      self._tmpdir = None
+      project = self._Compute(opt)
+    finally:
+      if project and opt.keep_git:
+        print 'GIT_DIR = %s' % (project.gitdir)
+      elif self._tmpdir:
+        for root, dirs, files in os.walk(self._tmpdir, topdown=False):
+          for name in files:
+            os.remove(os.path.join(root, name))
+          for name in dirs:
+            os.rmdir(os.path.join(root, name))
+        os.rmdir(self._tmpdir)
+
diff --git a/subcmds/diff.py b/subcmds/diff.py
new file mode 100644
index 0000000..e024714
--- /dev/null
+++ b/subcmds/diff.py
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from command import PagedCommand
+
+class Diff(PagedCommand):
+  common = True
+  helpSummary = "Show changes between commit and working tree"
+  helpUsage = """
+%prog [<project>...]
+"""
+
+  def Execute(self, opt, args):
+    for project in self.GetProjects(args):
+      project.PrintWorkTreeDiff()
diff --git a/subcmds/forall.py b/subcmds/forall.py
new file mode 100644
index 0000000..b22e22a
--- /dev/null
+++ b/subcmds/forall.py
@@ -0,0 +1,82 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import os
+import sys
+import subprocess
+from command import Command
+
+class Forall(Command):
+  common = False
+  helpSummary = "Run a shell command in each project"
+  helpUsage = """
+%prog [<project>...] -c <command> [<arg>...]
+"""
+  helpDescription = """
+Executes the same shell command in each project.
+
+Environment
+-----------
+pwd is the project's working directory.
+
+REPO_PROJECT is set to the unique name of the project.
+
+shell positional arguments ($1, $2, .., $#) are set to any arguments
+following <command>.
+
+stdin, stdout, stderr are inherited from the terminal and are
+not redirected.
+"""
+
+  def _Options(self, p):
+    def cmd(option, opt_str, value, parser):
+      setattr(parser.values, option.dest, list(parser.rargs))
+      while parser.rargs:
+        del parser.rargs[0]
+    p.add_option('-c', '--command',
+                 help='Command (and arguments) to execute',
+                 dest='command',
+                 action='callback',
+                 callback=cmd)
+
+  def Execute(self, opt, args):
+    if not opt.command:
+      self.Usage()
+
+    cmd = [opt.command[0]]
+
+    shell = True
+    if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]):
+      shell = False
+
+    if shell:
+      cmd.append(cmd[0])
+    cmd.extend(opt.command[1:])
+
+    rc = 0
+    for project in self.GetProjects(args):
+      env = dict(os.environ.iteritems())
+      env['REPO_PROJECT'] = project.name
+
+      p = subprocess.Popen(cmd,
+                           cwd = project.worktree,
+                           shell = shell,
+                           env = env)
+      r = p.wait()
+      if r != 0 and r != rc:
+        rc = r
+    if rc != 0:
+      sys.exit(rc)
diff --git a/subcmds/help.py b/subcmds/help.py
new file mode 100644
index 0000000..6e0238a
--- /dev/null
+++ b/subcmds/help.py
@@ -0,0 +1,147 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+from formatter import AbstractFormatter, DumbWriter
+
+from color import Coloring
+from command import PagedCommand
+
+class Help(PagedCommand):
+  common = False
+  helpSummary = "Display detailed help on a command"
+  helpUsage = """
+%prog [--all|command]
+"""
+  helpDescription = """
+Displays detailed usage information about a command.
+"""
+
+  def _PrintAllCommands(self):
+    print 'usage: repo COMMAND [ARGS]'
+    print """
+The complete list of recognized repo commands are:
+"""
+    commandNames = self.commands.keys()
+    commandNames.sort()
+
+    maxlen = 0
+    for name in commandNames:
+      maxlen = max(maxlen, len(name))
+    fmt = '  %%-%ds  %%s' % maxlen
+
+    for name in commandNames:
+      command = self.commands[name]
+      try:
+        summary = command.helpSummary.strip()
+      except AttributeError:
+        summary = ''
+      print fmt % (name, summary)
+    print """
+See 'repo help <command>' for more information on a specific command.
+"""
+
+  def _PrintCommonCommands(self):
+    print 'usage: repo COMMAND [ARGS]'
+    print """
+The most commonly used repo commands are:
+"""
+    commandNames = [name 
+                    for name in self.commands.keys()
+                    if self.commands[name].common]
+    commandNames.sort()
+
+    maxlen = 0
+    for name in commandNames:
+      maxlen = max(maxlen, len(name))
+    fmt = '  %%-%ds  %%s' % maxlen
+
+    for name in commandNames:
+      command = self.commands[name]
+      try:
+        summary = command.helpSummary.strip()
+      except AttributeError:
+        summary = ''
+      print fmt % (name, summary)
+    print """
+See 'repo help <command>' for more information on a specific command.
+"""
+
+  def _PrintCommandHelp(self, cmd):
+    class _Out(Coloring):
+      def __init__(self, gc):
+        Coloring.__init__(self, gc, 'help')
+        self.heading = self.printer('heading', attr='bold')
+
+        self.wrap = AbstractFormatter(DumbWriter())
+
+      def _PrintSection(self, heading, bodyAttr):
+        try:
+          body = getattr(cmd, bodyAttr)
+        except AttributeError:
+          return
+
+        self.nl()
+
+        self.heading('%s', heading)
+        self.nl()
+
+        self.heading('%s', ''.ljust(len(heading), '-'))
+        self.nl()
+
+        me = 'repo %s' % cmd.NAME
+        body = body.strip()
+        body = body.replace('%prog', me)
+
+        for para in body.split("\n\n"):
+          if para.startswith(' '):
+            self.write('%s', para)
+            self.nl()
+            self.nl()
+          else:
+            self.wrap.add_flowing_data(para)
+            self.wrap.end_paragraph(1)
+        self.wrap.end_paragraph(0)
+
+    out = _Out(self.manifest.globalConfig)
+    cmd.OptionParser.print_help()
+    out._PrintSection('Summary', 'helpSummary')
+    out._PrintSection('Description', 'helpDescription')
+
+  def _Options(self, p):
+    p.add_option('-a', '--all',
+                 dest='show_all', action='store_true',
+                 help='show the complete list of commands')
+
+  def Execute(self, opt, args):
+    if len(args) == 0:
+      if opt.show_all:
+        self._PrintAllCommands()
+      else:
+        self._PrintCommonCommands()
+
+    elif len(args) == 1:
+      name = args[0]
+
+      try:
+        cmd = self.commands[name]
+      except KeyError:
+        print >>sys.stderr, "repo: '%s' is not a repo command." % name
+        sys.exit(1)
+
+      self._PrintCommandHelp(cmd)
+
+    else:
+      self._PrintCommandHelp(self)
diff --git a/subcmds/init.py b/subcmds/init.py
new file mode 100644
index 0000000..03f358d
--- /dev/null
+++ b/subcmds/init.py
@@ -0,0 +1,193 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+from color import Coloring
+from command import InteractiveCommand
+from error import ManifestParseError
+from remote import Remote
+from git_command import git, MIN_GIT_VERSION
+
+class Init(InteractiveCommand):
+  common = True
+  helpSummary = "Initialize repo in the current directory"
+  helpUsage = """
+%prog [options]
+"""
+  helpDescription = """
+The '%prog' command is run once to install and initialize repo.
+The latest repo source code and manifest collection is downloaded
+from the server and is installed in the .repo/ directory in the
+current working directory.
+
+The optional <manifest> argument can be used to specify an alternate
+manifest to be used. If no manifest is specified, the manifest
+default.xml will be used.
+"""
+
+  def _Options(self, p):
+    # Logging
+    g = p.add_option_group('Logging options')
+    g.add_option('-q', '--quiet',
+                 dest="quiet", action="store_true", default=False,
+                 help="be quiet")
+
+    # Manifest
+    g = p.add_option_group('Manifest options')
+    g.add_option('-u', '--manifest-url',
+                 dest='manifest_url',
+                 help='manifest repository location', metavar='URL')
+    g.add_option('-b', '--manifest-branch',
+                 dest='manifest_branch',
+                 help='manifest branch or revision', metavar='REVISION')
+    g.add_option('-m', '--manifest-name',
+                 dest='manifest_name', default='default.xml',
+                 help='initial manifest file', metavar='NAME.xml')
+
+    # Tool
+    g = p.add_option_group('Version options')
+    g.add_option('--repo-url',
+                 dest='repo_url',
+                 help='repo repository location', metavar='URL')
+    g.add_option('--repo-branch',
+                 dest='repo_branch',
+                 help='repo branch or revision', metavar='REVISION')
+    g.add_option('--no-repo-verify',
+                 dest='no_repo_verify', action='store_true',
+                 help='do not verify repo source code')
+
+  def _CheckGitVersion(self):
+    ver_str = git.version()
+    if not ver_str.startswith('git version '):
+      print >>sys.stderr, 'error: "%s" unsupported' % ver_str
+      sys.exit(1)
+
+    ver_str = ver_str[len('git version '):].strip()
+    ver_act = tuple(map(lambda x: int(x), ver_str.split('.')[0:3]))
+    if ver_act < MIN_GIT_VERSION:
+      need = '.'.join(map(lambda x: str(x), MIN_GIT_VERSION))
+      print >>sys.stderr, 'fatal: git %s or later required' % need
+      sys.exit(1)
+
+  def _SyncManifest(self, opt):
+    m = self.manifest.manifestProject
+
+    if not m.Exists:
+      if not opt.manifest_url:
+        print >>sys.stderr, 'fatal: manifest url (-u) is required.'
+        sys.exit(1)
+
+      if not opt.quiet:
+        print >>sys.stderr, 'Getting manifest ...'
+        print >>sys.stderr, '   from %s' % opt.manifest_url
+      m._InitGitDir()
+
+      if opt.manifest_branch:
+        m.revision = opt.manifest_branch
+      else:
+        m.revision = 'refs/heads/master'
+    else:
+      if opt.manifest_branch:
+        m.revision = opt.manifest_branch
+      else:
+        m.PreSync()
+
+    if opt.manifest_url:
+      r = m.GetRemote(m.remote.name)
+      r.url = opt.manifest_url
+      r.ResetFetch()
+      r.Save()
+
+    m.Sync_NetworkHalf()
+    m.Sync_LocalHalf()
+    m.StartBranch('default')
+
+  def _LinkManifest(self, name):
+    if not name:
+      print >>sys.stderr, 'fatal: manifest name (-m) is required.'
+      sys.exit(1)
+
+    try:
+      self.manifest.Link(name)
+    except ManifestParseError, e:
+      print >>sys.stderr, "fatal: manifest '%s' not available" % name
+      print >>sys.stderr, 'fatal: %s' % str(e)
+      sys.exit(1)
+
+  def _PromptKey(self, prompt, key, value):
+    mp = self.manifest.manifestProject
+
+    sys.stdout.write('%-10s [%s]: ' % (prompt, value))
+    a = sys.stdin.readline().strip()
+    if a != '' and a != value:
+      mp.config.SetString(key, a)
+
+  def _ConfigureUser(self):
+    mp = self.manifest.manifestProject
+
+    print ''
+    self._PromptKey('Your Name', 'user.name', mp.UserName)
+    self._PromptKey('Your Email', 'user.email', mp.UserEmail)
+
+  def _HasColorSet(self, gc):
+    for n in ['ui', 'diff', 'status']:
+      if gc.Has('color.%s' % n):
+        return True
+    return False
+
+  def _ConfigureColor(self):
+    gc = self.manifest.globalConfig
+    if self._HasColorSet(gc):
+      return
+
+    class _Test(Coloring):
+      def __init__(self):
+        Coloring.__init__(self, gc, 'test color display')
+        self._on = True
+    out = _Test()
+
+    print ''
+    print "Testing colorized output (for 'repo diff', 'repo status'):"
+
+    for c in ['black','red','green','yellow','blue','magenta','cyan']:
+      out.write(' ')
+      out.printer(fg=c)(' %-6s ', c)
+    out.write(' ')
+    out.printer(fg='white', bg='black')(' %s ' % 'white')
+    out.nl()
+
+    for c in ['bold','dim','ul','reverse']:
+      out.write(' ')
+      out.printer(fg='black', attr=c)(' %-6s ', c)
+    out.nl()
+
+    sys.stdout.write('Enable color display in this user account (y/n)? ')
+    a = sys.stdin.readline().strip().lower()
+    if a in ('y', 'yes', 't', 'true', 'on'):
+      gc.SetString('color.ui', 'auto')
+
+  def Execute(self, opt, args):
+    self._CheckGitVersion()
+    self._SyncManifest(opt)
+    self._LinkManifest(opt.manifest_name)
+
+    if os.isatty(0) and os.isatty(1):
+      self._ConfigureUser()
+      self._ConfigureColor()
+
+    print ''
+    print 'repo initialized in %s' % self.manifest.topdir
diff --git a/subcmds/prune.py b/subcmds/prune.py
new file mode 100644
index 0000000..f412bd4
--- /dev/null
+++ b/subcmds/prune.py
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from color import Coloring
+from command import PagedCommand
+
+class Prune(PagedCommand):
+  common = True
+  helpSummary = "Prune (delete) already merged topics"
+  helpUsage = """
+%prog [<project>...]
+"""
+
+  def Execute(self, opt, args):
+    all = []
+    for project in self.GetProjects(args):
+      all.extend(project.PruneHeads())
+
+    if not all:
+      return
+
+    class Report(Coloring):
+      def __init__(self, config):
+        Coloring.__init__(self, config, 'status')
+        self.project = self.printer('header', attr='bold')
+
+    out = Report(all[0].project.config)
+    out.project('Pending Branches')
+    out.nl()
+
+    project = None
+
+    for branch in all:
+      if project != branch.project:
+        project = branch.project
+        out.nl()
+        out.project('project %s/' % project.relpath)
+        out.nl()
+
+      commits = branch.commits
+      date = branch.date
+      print '%s %-33s (%2d commit%s, %s)' % (
+            branch.name == project.CurrentBranch and '*' or ' ',
+            branch.name,
+            len(commits),
+            len(commits) != 1 and 's' or ' ',
+            date)
diff --git a/subcmds/stage.py b/subcmds/stage.py
new file mode 100644
index 0000000..c451cd6
--- /dev/null
+++ b/subcmds/stage.py
@@ -0,0 +1,108 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+from color import Coloring
+from command import InteractiveCommand
+from git_command import GitCommand
+
+class _ProjectList(Coloring):
+  def __init__(self, gc):
+    Coloring.__init__(self, gc, 'interactive')
+    self.prompt = self.printer('prompt', fg='blue', attr='bold')
+    self.header = self.printer('header', attr='bold')
+    self.help = self.printer('help', fg='red', attr='bold')
+
+class Stage(InteractiveCommand):
+  common = True
+  helpSummary = "Stage file(s) for commit"
+  helpUsage = """
+%prog -i [<project>...]
+"""
+  helpDescription = """
+The '%prog' command stages files to prepare the next commit.
+"""
+
+  def _Options(self, p):
+    p.add_option('-i', '--interactive',
+                 dest='interactive', action='store_true',
+                 help='use interactive staging')
+
+  def Execute(self, opt, args):
+    if opt.interactive:
+      self._Interactive(opt, args)
+    else:
+      self.Usage()
+
+  def _Interactive(self, opt, args):
+    all = filter(lambda x: x.IsDirty(), self.GetProjects(args))
+    if not all:
+      print >>sys.stderr,'no projects have uncommitted modifications'
+      return
+
+    out = _ProjectList(self.manifest.manifestProject.config)
+    while True:
+      out.header('        %-20s %s', 'project', 'path')
+      out.nl()
+
+      for i in xrange(0, len(all)):
+        p = all[i]
+        out.write('%3d:    %-20s %s', i + 1, p.name, p.relpath + '/')
+        out.nl()
+      out.nl()
+
+      out.write('%3d: (', 0)
+      out.prompt('q')
+      out.write('uit)')
+      out.nl()
+
+      out.prompt('project> ')
+      try:
+        a = sys.stdin.readline()
+      except KeyboardInterrupt:
+        out.nl()
+        break
+      if a == '':
+        out.nl()
+        break
+
+      a = a.strip()
+      if a.lower() in ('q', 'quit', 'exit'):
+        break
+      if not a:
+        continue
+
+      try:
+        a_index = int(a)
+      except ValueError:
+        a_index = None
+
+      if a_index is not None:
+        if a_index == 0:
+          break
+        if 0 < a_index and a_index <= len(all):
+          _AddI(all[a_index - 1])
+          continue
+
+      p = filter(lambda x: x.name == a or x.relpath == a, all)
+      if len(p) == 1:
+        _AddI(p[0])
+        continue
+    print 'Bye.'
+
+def _AddI(project):
+  p = GitCommand(project, ['add', '--interactive'], bare=False)
+  p.Wait()
diff --git a/subcmds/start.py b/subcmds/start.py
new file mode 100644
index 0000000..4eb3e47
--- /dev/null
+++ b/subcmds/start.py
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+from command import Command
+from git_command import git
+
+class Start(Command):
+  common = True
+  helpSummary = "Start a new branch for development"
+  helpUsage = """
+%prog <newbranchname> [<project>...]
+
+This subcommand starts a new branch of development that is automatically
+pulled from a remote branch.
+
+It is equivalent to the following git commands:
+
+"git branch --track <newbranchname> m/<codeline>",
+or 
+"git checkout --track -b <newbranchname> m/<codeline>".
+
+All three forms set up the config entries that repo bases some of its
+processing on.  Use %prog or git branch or checkout with --track to ensure
+the configuration data is set up properly.
+
+"""
+
+  def Execute(self, opt, args):
+    if not args:
+      self.Usage()
+
+    nb = args[0]
+    if not git.check_ref_format('heads/%s' % nb):
+      print >>sys.stderr, "error: '%s' is not a valid name" % nb
+      sys.exit(1)
+
+    for project in self.GetProjects(args[1:]):
+      project.StartBranch(nb)
diff --git a/subcmds/status.py b/subcmds/status.py
new file mode 100644
index 0000000..1615b42
--- /dev/null
+++ b/subcmds/status.py
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from command import PagedCommand
+
+class Status(PagedCommand):
+  common = True
+  helpSummary = "Show the working tree status"
+  helpUsage = """
+%prog [<project>...]
+"""
+
+  def Execute(self, opt, args):
+    for project in self.GetProjects(args):
+      project.PrintWorkTreeStatus()
diff --git a/subcmds/sync.py b/subcmds/sync.py
new file mode 100644
index 0000000..3eb44ed
--- /dev/null
+++ b/subcmds/sync.py
@@ -0,0 +1,150 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+import subprocess
+import sys
+
+from git_command import GIT
+from command import Command
+from error import RepoChangedException, GitError
+from project import R_HEADS
+
+class Sync(Command):
+  common = True
+  helpSummary = "Update working tree to the latest revision"
+  helpUsage = """
+%prog [<project>...]
+"""
+  helpDescription = """
+The '%prog' command synchronizes local project directories
+with the remote repositories specified in the manifest.  If a local
+project does not yet exist, it will clone a new local directory from
+the remote repository and set up tracking branches as specified in
+the manifest.  If the local project already exists, '%prog'
+will update the remote branches and rebase any new local changes
+on top of the new remote changes.
+
+'%prog' will synchronize all projects listed at the command
+line.  Projects can be specified either by name, or by a relative
+or absolute path to the project's local directory. If no projects
+are specified, '%prog' will synchronize all projects listed in
+the manifest.
+"""
+
+  def _Options(self, p):
+    p.add_option('--no-repo-verify',
+                 dest='no_repo_verify', action='store_true',
+                 help='do not verify repo source code')
+
+  def _Fetch(self, *projects):
+    fetched = set()
+    for project in projects:
+      if project.Sync_NetworkHalf():
+        fetched.add(project.gitdir)
+      else:
+        print >>sys.stderr, 'error: Cannot fetch %s' % project.name
+        sys.exit(1)
+    return fetched
+
+  def Execute(self, opt, args):
+    rp = self.manifest.repoProject
+    rp.PreSync()
+
+    mp = self.manifest.manifestProject
+    mp.PreSync()
+
+    all = self.GetProjects(args, missing_ok=True)
+    fetched = self._Fetch(rp, mp, *all)
+
+    if rp.HasChanges:
+      print >>sys.stderr, 'info: A new version of repo is available'
+      print >>sys.stderr, ''
+      if opt.no_repo_verify or _VerifyTag(rp):
+        if not rp.Sync_LocalHalf():
+          sys.exit(1)
+        print >>sys.stderr, 'info: Restarting repo with latest version'
+        raise RepoChangedException()
+      else:
+        print >>sys.stderr, 'warning: Skipped upgrade to unverified version'
+
+    if mp.HasChanges:
+      if not mp.Sync_LocalHalf():
+        sys.exit(1)
+
+      self.manifest._Unload()
+      all = self.GetProjects(args, missing_ok=True)
+      missing = []
+      for project in all:
+        if project.gitdir not in fetched:
+          missing.append(project)
+      self._Fetch(*missing)
+
+    for project in all:
+      if not project.Sync_LocalHalf():
+        sys.exit(1)
+
+
+def _VerifyTag(project):
+  gpg_dir = os.path.expanduser('~/.repoconfig/gnupg')
+  if not os.path.exists(gpg_dir):
+    print >>sys.stderr,\
+"""warning: GnuPG was not available during last "repo init"
+warning: Cannot automatically authenticate repo."""
+    return True
+
+  remote = project.GetRemote(project.remote.name)
+  ref = remote.ToLocal(project.revision)
+
+  try:
+    cur = project.bare_git.describe(ref)
+  except GitError:
+    cur = None
+
+  if not cur \
+     or re.compile(r'^.*-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur):
+    rev = project.revision
+    if rev.startswith(R_HEADS):
+      rev = rev[len(R_HEADS):]
+
+    print >>sys.stderr
+    print >>sys.stderr,\
+      "warning: project '%s' branch '%s' is not signed" \
+      % (project.name, rev)
+    return False
+
+  env = dict(os.environ)
+  env['GIT_DIR'] = project.gitdir
+  env['GNUPGHOME'] = gpg_dir
+
+  cmd = [GIT, 'tag', '-v', cur]
+  proc = subprocess.Popen(cmd,
+                          stdout = subprocess.PIPE,
+                          stderr = subprocess.PIPE,
+                          env = env)
+  out = proc.stdout.read()
+  proc.stdout.close()
+
+  err = proc.stderr.read()
+  proc.stderr.close()
+
+  if proc.wait() != 0:
+    print >>sys.stderr
+    print >>sys.stderr, out
+    print >>sys.stderr, err
+    print >>sys.stderr
+    return False
+  return True
diff --git a/subcmds/upload.py b/subcmds/upload.py
new file mode 100644
index 0000000..ad05050
--- /dev/null
+++ b/subcmds/upload.py
@@ -0,0 +1,180 @@
+#
+# Copyright (C) 2008 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import sys
+
+from command import InteractiveCommand
+from editor import Editor
+from error import UploadError
+
+def _die(fmt, *args):
+  msg = fmt % args
+  print >>sys.stderr, 'error: %s' % msg
+  sys.exit(1)
+
+class Upload(InteractiveCommand):
+  common = True
+  helpSummary = "Upload changes for code review"
+  helpUsage="""
+%prog [<project>]...
+"""
+  helpDescription = """
+The '%prog' command is used to send changes to the Gerrit code
+review system.  It searches for changes in local projects that do
+not yet exist in the corresponding remote repository.  If multiple
+changes are found, '%prog' opens an editor to allow the
+user to choose which change to upload.  After a successful upload,
+repo prints the URL for the change in the Gerrit code review system.
+
+'%prog' searches for uploadable changes in all projects listed
+at the command line.  Projects can be specified either by name, or
+by a relative or absolute path to the project's local directory. If
+no projects are specified, '%prog' will search for uploadable
+changes in all projects listed in the manifest.
+"""
+
+  def _SingleBranch(self, branch):
+    project = branch.project
+    name = branch.name
+    date = branch.date
+    list = branch.commits
+
+    print 'Upload project %s/:' % project.relpath
+    print '  branch %s (%2d commit%s, %s):' % (
+                  name,
+                  len(list),
+                  len(list) != 1 and 's' or '',
+                  date)
+    for commit in list:
+      print '         %s' % commit
+
+    sys.stdout.write('(y/n)? ')
+    answer = sys.stdin.readline().strip()
+    if answer in ('y', 'Y', 'yes', '1', 'true', 't'):
+      self._UploadAndReport([branch])
+    else:
+      _die("upload aborted by user")
+
+  def _MultipleBranches(self, pending):
+    projects = {}
+    branches = {}
+
+    script = []
+    script.append('# Uncomment the branches to upload:')
+    for project, avail in pending:
+      script.append('#')
+      script.append('# project %s/:' % project.relpath)
+
+      b = {}
+      for branch in avail:
+        name = branch.name
+        date = branch.date
+        list = branch.commits
+
+        if b:
+          script.append('#')
+        script.append('#  branch %s (%2d commit%s, %s):' % (
+                      name,
+                      len(list),
+                      len(list) != 1 and 's' or '',
+                      date))
+        for commit in list:
+          script.append('#         %s' % commit)
+        b[name] = branch
+
+      projects[project.relpath] = project
+      branches[project.name] = b
+    script.append('')
+
+    script = Editor.EditString("\n".join(script)).split("\n")
+
+    project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
+    branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*')
+
+    project = None
+    todo = []
+
+    for line in script:
+      m = project_re.match(line)
+      if m:
+        name = m.group(1)
+        project = projects.get(name)
+        if not project:
+          _die('project %s not available for upload', name)
+        continue
+
+      m = branch_re.match(line)
+      if m:
+        name = m.group(1)
+        if not project:
+          _die('project for branch %s not in script', name)
+        branch = branches[project.name].get(name)
+        if not branch:
+          _die('branch %s not in %s', name, project.relpath)
+        todo.append(branch)
+    if not todo:
+      _die("nothing uncommented for upload")
+    self._UploadAndReport(todo)
+
+  def _UploadAndReport(self, todo):
+    have_errors = False
+    for branch in todo:
+      try:
+        branch.UploadForReview()
+        branch.uploaded = True
+      except UploadError, e:
+        branch.error = e
+        branch.uploaded = False
+        have_errors = True
+
+    print >>sys.stderr, ''
+    print >>sys.stderr, '--------------------------------------------'
+
+    if have_errors:
+      for branch in todo:
+        if not branch.uploaded:
+          print >>sys.stderr, '[FAILED] %-15s %-15s  (%s)' % (
+                 branch.project.relpath + '/', \
+                 branch.name, \
+                 branch.error)
+      print >>sys.stderr, ''
+
+    for branch in todo:
+        if branch.uploaded:
+          print >>sys.stderr, '[OK    ] %-15s %s' % (
+                 branch.project.relpath + '/',
+                 branch.name)
+          print >>sys.stderr, '%s' % branch.tip_url
+          print >>sys.stderr, ''
+
+    if have_errors:
+      sys.exit(1)
+
+  def Execute(self, opt, args):
+    project_list = self.GetProjects(args)
+    pending = []
+
+    for project in project_list:
+      avail = project.GetUploadableBranches()
+      if avail:
+        pending.append((project, avail))
+
+    if not pending:
+      print >>sys.stdout, "no branches ready for upload"
+    elif len(pending) == 1 and len(pending[0][1]) == 1:
+      self._SingleBranch(pending[0][1][0])
+    else:
+      self._MultipleBranches(pending)