Blame googletest/scripts/upload.py

Packit bd1cd8
#!/usr/bin/env python
Packit bd1cd8
#
Packit bd1cd8
# Copyright 2007 Google Inc.
Packit bd1cd8
#
Packit bd1cd8
# Licensed under the Apache License, Version 2.0 (the "License");
Packit bd1cd8
# you may not use this file except in compliance with the License.
Packit bd1cd8
# You may obtain a copy of the License at
Packit bd1cd8
#
Packit bd1cd8
#     http://www.apache.org/licenses/LICENSE-2.0
Packit bd1cd8
#
Packit bd1cd8
# Unless required by applicable law or agreed to in writing, software
Packit bd1cd8
# distributed under the License is distributed on an "AS IS" BASIS,
Packit bd1cd8
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Packit bd1cd8
# See the License for the specific language governing permissions and
Packit bd1cd8
# limitations under the License.
Packit bd1cd8
Packit bd1cd8
"""Tool for uploading diffs from a version control system to the codereview app.
Packit bd1cd8
Packit bd1cd8
Usage summary: upload.py [options] [-- diff_options]
Packit bd1cd8
Packit bd1cd8
Diff options are passed to the diff command of the underlying system.
Packit bd1cd8
Packit bd1cd8
Supported version control systems:
Packit bd1cd8
  Git
Packit bd1cd8
  Mercurial
Packit bd1cd8
  Subversion
Packit bd1cd8
Packit bd1cd8
It is important for Git/Mercurial users to specify a tree/node/branch to diff
Packit bd1cd8
against by using the '--rev' option.
Packit bd1cd8
"""
Packit bd1cd8
# This code is derived from appcfg.py in the App Engine SDK (open source),
Packit bd1cd8
# and from ASPN recipe #146306.
Packit bd1cd8
Packit bd1cd8
import cookielib
Packit bd1cd8
import getpass
Packit bd1cd8
import logging
Packit bd1cd8
import md5
Packit bd1cd8
import mimetypes
Packit bd1cd8
import optparse
Packit bd1cd8
import os
Packit bd1cd8
import re
Packit bd1cd8
import socket
Packit bd1cd8
import subprocess
Packit bd1cd8
import sys
Packit bd1cd8
import urllib
Packit bd1cd8
import urllib2
Packit bd1cd8
import urlparse
Packit bd1cd8
Packit bd1cd8
try:
Packit bd1cd8
  import readline
Packit bd1cd8
except ImportError:
Packit bd1cd8
  pass
Packit bd1cd8
Packit bd1cd8
# The logging verbosity:
Packit bd1cd8
#  0: Errors only.
Packit bd1cd8
#  1: Status messages.
Packit bd1cd8
#  2: Info logs.
Packit bd1cd8
#  3: Debug logs.
Packit bd1cd8
verbosity = 1
Packit bd1cd8
Packit bd1cd8
# Max size of patch or base file.
Packit bd1cd8
MAX_UPLOAD_SIZE = 900 * 1024
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def GetEmail(prompt):
Packit bd1cd8
  """Prompts the user for their email address and returns it.
Packit bd1cd8
Packit bd1cd8
  The last used email address is saved to a file and offered up as a suggestion
Packit bd1cd8
  to the user. If the user presses enter without typing in anything the last
Packit bd1cd8
  used email address is used. If the user enters a new address, it is saved
Packit bd1cd8
  for next time we prompt.
Packit bd1cd8
Packit bd1cd8
  """
Packit bd1cd8
  last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
Packit bd1cd8
  last_email = ""
Packit bd1cd8
  if os.path.exists(last_email_file_name):
Packit bd1cd8
    try:
Packit bd1cd8
      last_email_file = open(last_email_file_name, "r")
Packit bd1cd8
      last_email = last_email_file.readline().strip("\n")
Packit bd1cd8
      last_email_file.close()
Packit bd1cd8
      prompt += " [%s]" % last_email
Packit bd1cd8
    except IOError, e:
Packit bd1cd8
      pass
Packit bd1cd8
  email = raw_input(prompt + ": ").strip()
Packit bd1cd8
  if email:
Packit bd1cd8
    try:
Packit bd1cd8
      last_email_file = open(last_email_file_name, "w")
Packit bd1cd8
      last_email_file.write(email)
Packit bd1cd8
      last_email_file.close()
Packit bd1cd8
    except IOError, e:
Packit bd1cd8
      pass
Packit bd1cd8
  else:
Packit bd1cd8
    email = last_email
Packit bd1cd8
  return email
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def StatusUpdate(msg):
Packit bd1cd8
  """Print a status message to stdout.
Packit bd1cd8
Packit bd1cd8
  If 'verbosity' is greater than 0, print the message.
Packit bd1cd8
Packit bd1cd8
  Args:
Packit bd1cd8
    msg: The string to print.
Packit bd1cd8
  """
Packit bd1cd8
  if verbosity > 0:
Packit bd1cd8
    print msg
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def ErrorExit(msg):
Packit bd1cd8
  """Print an error message to stderr and exit."""
Packit bd1cd8
  print >>sys.stderr, msg
Packit bd1cd8
  sys.exit(1)
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class ClientLoginError(urllib2.HTTPError):
Packit bd1cd8
  """Raised to indicate there was an error authenticating with ClientLogin."""
Packit bd1cd8
Packit bd1cd8
  def __init__(self, url, code, msg, headers, args):
Packit bd1cd8
    urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
Packit bd1cd8
    self.args = args
Packit bd1cd8
    self.reason = args["Error"]
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class AbstractRpcServer(object):
Packit bd1cd8
  """Provides a common interface for a simple RPC server."""
Packit bd1cd8
Packit bd1cd8
  def __init__(self, host, auth_function, host_override=None, extra_headers={},
Packit bd1cd8
               save_cookies=False):
Packit bd1cd8
    """Creates a new HttpRpcServer.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      host: The host to send requests to.
Packit bd1cd8
      auth_function: A function that takes no arguments and returns an
Packit bd1cd8
        (email, password) tuple when called. Will be called if authentication
Packit bd1cd8
        is required.
Packit bd1cd8
      host_override: The host header to send to the server (defaults to host).
Packit bd1cd8
      extra_headers: A dict of extra headers to append to every request.
Packit bd1cd8
      save_cookies: If True, save the authentication cookies to local disk.
Packit bd1cd8
        If False, use an in-memory cookiejar instead.  Subclasses must
Packit bd1cd8
        implement this functionality.  Defaults to False.
Packit bd1cd8
    """
Packit bd1cd8
    self.host = host
Packit bd1cd8
    self.host_override = host_override
Packit bd1cd8
    self.auth_function = auth_function
Packit bd1cd8
    self.authenticated = False
Packit bd1cd8
    self.extra_headers = extra_headers
Packit bd1cd8
    self.save_cookies = save_cookies
Packit bd1cd8
    self.opener = self._GetOpener()
Packit bd1cd8
    if self.host_override:
Packit bd1cd8
      logging.info("Server: %s; Host: %s", self.host, self.host_override)
Packit bd1cd8
    else:
Packit bd1cd8
      logging.info("Server: %s", self.host)
Packit bd1cd8
Packit bd1cd8
  def _GetOpener(self):
Packit bd1cd8
    """Returns an OpenerDirector for making HTTP requests.
Packit bd1cd8
Packit bd1cd8
    Returns:
Packit bd1cd8
      A urllib2.OpenerDirector object.
Packit bd1cd8
    """
Packit bd1cd8
    raise NotImplementedError()
Packit bd1cd8
Packit bd1cd8
  def _CreateRequest(self, url, data=None):
Packit bd1cd8
    """Creates a new urllib request."""
Packit bd1cd8
    logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
Packit bd1cd8
    req = urllib2.Request(url, data=data)
Packit bd1cd8
    if self.host_override:
Packit bd1cd8
      req.add_header("Host", self.host_override)
Packit bd1cd8
    for key, value in self.extra_headers.iteritems():
Packit bd1cd8
      req.add_header(key, value)
Packit bd1cd8
    return req
Packit bd1cd8
Packit bd1cd8
  def _GetAuthToken(self, email, password):
Packit bd1cd8
    """Uses ClientLogin to authenticate the user, returning an auth token.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      email:    The user's email address
Packit bd1cd8
      password: The user's password
Packit bd1cd8
Packit bd1cd8
    Raises:
Packit bd1cd8
      ClientLoginError: If there was an error authenticating with ClientLogin.
Packit bd1cd8
      HTTPError: If there was some other form of HTTP error.
Packit bd1cd8
Packit bd1cd8
    Returns:
Packit bd1cd8
      The authentication token returned by ClientLogin.
Packit bd1cd8
    """
Packit bd1cd8
    account_type = "GOOGLE"
Packit bd1cd8
    if self.host.endswith(".google.com"):
Packit bd1cd8
      # Needed for use inside Google.
Packit bd1cd8
      account_type = "HOSTED"
Packit bd1cd8
    req = self._CreateRequest(
Packit bd1cd8
        url="https://www.google.com/accounts/ClientLogin",
Packit bd1cd8
        data=urllib.urlencode({
Packit bd1cd8
            "Email": email,
Packit bd1cd8
            "Passwd": password,
Packit bd1cd8
            "service": "ah",
Packit bd1cd8
            "source": "rietveld-codereview-upload",
Packit bd1cd8
            "accountType": account_type,
Packit bd1cd8
        }),
Packit bd1cd8
    )
Packit bd1cd8
    try:
Packit bd1cd8
      response = self.opener.open(req)
Packit bd1cd8
      response_body = response.read()
Packit bd1cd8
      response_dict = dict(x.split("=")
Packit bd1cd8
                           for x in response_body.split("\n") if x)
Packit bd1cd8
      return response_dict["Auth"]
Packit bd1cd8
    except urllib2.HTTPError, e:
Packit bd1cd8
      if e.code == 403:
Packit bd1cd8
        body = e.read()
Packit bd1cd8
        response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
Packit bd1cd8
        raise ClientLoginError(req.get_full_url(), e.code, e.msg,
Packit bd1cd8
                               e.headers, response_dict)
Packit bd1cd8
      else:
Packit bd1cd8
        raise
Packit bd1cd8
Packit bd1cd8
  def _GetAuthCookie(self, auth_token):
Packit bd1cd8
    """Fetches authentication cookies for an authentication token.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      auth_token: The authentication token returned by ClientLogin.
Packit bd1cd8
Packit bd1cd8
    Raises:
Packit bd1cd8
      HTTPError: If there was an error fetching the authentication cookies.
Packit bd1cd8
    """
Packit bd1cd8
    # This is a dummy value to allow us to identify when we're successful.
Packit bd1cd8
    continue_location = "http://localhost/"
Packit bd1cd8
    args = {"continue": continue_location, "auth": auth_token}
Packit bd1cd8
    req = self._CreateRequest("http://%s/_ah/login?%s" %
Packit bd1cd8
                              (self.host, urllib.urlencode(args)))
Packit bd1cd8
    try:
Packit bd1cd8
      response = self.opener.open(req)
Packit bd1cd8
    except urllib2.HTTPError, e:
Packit bd1cd8
      response = e
Packit bd1cd8
    if (response.code != 302 or
Packit bd1cd8
        response.info()["location"] != continue_location):
Packit bd1cd8
      raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
Packit bd1cd8
                              response.headers, response.fp)
Packit bd1cd8
    self.authenticated = True
Packit bd1cd8
Packit bd1cd8
  def _Authenticate(self):
Packit bd1cd8
    """Authenticates the user.
Packit bd1cd8
Packit bd1cd8
    The authentication process works as follows:
Packit bd1cd8
     1) We get a username and password from the user
Packit bd1cd8
     2) We use ClientLogin to obtain an AUTH token for the user
Packit bd1cd8
        (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
Packit bd1cd8
     3) We pass the auth token to /_ah/login on the server to obtain an
Packit bd1cd8
        authentication cookie. If login was successful, it tries to redirect
Packit bd1cd8
        us to the URL we provided.
Packit bd1cd8
Packit bd1cd8
    If we attempt to access the upload API without first obtaining an
Packit bd1cd8
    authentication cookie, it returns a 401 response and directs us to
Packit bd1cd8
    authenticate ourselves with ClientLogin.
Packit bd1cd8
    """
Packit bd1cd8
    for i in range(3):
Packit bd1cd8
      credentials = self.auth_function()
Packit bd1cd8
      try:
Packit bd1cd8
        auth_token = self._GetAuthToken(credentials[0], credentials[1])
Packit bd1cd8
      except ClientLoginError, e:
Packit bd1cd8
        if e.reason == "BadAuthentication":
Packit bd1cd8
          print >>sys.stderr, "Invalid username or password."
Packit bd1cd8
          continue
Packit bd1cd8
        if e.reason == "CaptchaRequired":
Packit bd1cd8
          print >>sys.stderr, (
Packit bd1cd8
              "Please go to\n"
Packit bd1cd8
              "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
Packit bd1cd8
              "and verify you are a human.  Then try again.")
Packit bd1cd8
          break
Packit bd1cd8
        if e.reason == "NotVerified":
Packit bd1cd8
          print >>sys.stderr, "Account not verified."
Packit bd1cd8
          break
Packit bd1cd8
        if e.reason == "TermsNotAgreed":
Packit bd1cd8
          print >>sys.stderr, "User has not agreed to TOS."
Packit bd1cd8
          break
Packit bd1cd8
        if e.reason == "AccountDeleted":
Packit bd1cd8
          print >>sys.stderr, "The user account has been deleted."
Packit bd1cd8
          break
Packit bd1cd8
        if e.reason == "AccountDisabled":
Packit bd1cd8
          print >>sys.stderr, "The user account has been disabled."
Packit bd1cd8
          break
Packit bd1cd8
        if e.reason == "ServiceDisabled":
Packit bd1cd8
          print >>sys.stderr, ("The user's access to the service has been "
Packit bd1cd8
                               "disabled.")
Packit bd1cd8
          break
Packit bd1cd8
        if e.reason == "ServiceUnavailable":
Packit bd1cd8
          print >>sys.stderr, "The service is not available; try again later."
Packit bd1cd8
          break
Packit bd1cd8
        raise
Packit bd1cd8
      self._GetAuthCookie(auth_token)
Packit bd1cd8
      return
Packit bd1cd8
Packit bd1cd8
  def Send(self, request_path, payload=None,
Packit bd1cd8
           content_type="application/octet-stream",
Packit bd1cd8
           timeout=None,
Packit bd1cd8
           **kwargs):
Packit bd1cd8
    """Sends an RPC and returns the response.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      request_path: The path to send the request to, eg /api/appversion/create.
Packit bd1cd8
      payload: The body of the request, or None to send an empty request.
Packit bd1cd8
      content_type: The Content-Type header to use.
Packit bd1cd8
      timeout: timeout in seconds; default None i.e. no timeout.
Packit bd1cd8
        (Note: for large requests on OS X, the timeout doesn't work right.)
Packit bd1cd8
      kwargs: Any keyword arguments are converted into query string parameters.
Packit bd1cd8
Packit bd1cd8
    Returns:
Packit bd1cd8
      The response body, as a string.
Packit bd1cd8
    """
Packit bd1cd8
    # TODO: Don't require authentication.  Let the server say
Packit bd1cd8
    # whether it is necessary.
Packit bd1cd8
    if not self.authenticated:
Packit bd1cd8
      self._Authenticate()
Packit bd1cd8
Packit bd1cd8
    old_timeout = socket.getdefaulttimeout()
Packit bd1cd8
    socket.setdefaulttimeout(timeout)
Packit bd1cd8
    try:
Packit bd1cd8
      tries = 0
Packit bd1cd8
      while True:
Packit bd1cd8
        tries += 1
Packit bd1cd8
        args = dict(kwargs)
Packit bd1cd8
        url = "http://%s%s" % (self.host, request_path)
Packit bd1cd8
        if args:
Packit bd1cd8
          url += "?" + urllib.urlencode(args)
Packit bd1cd8
        req = self._CreateRequest(url=url, data=payload)
Packit bd1cd8
        req.add_header("Content-Type", content_type)
Packit bd1cd8
        try:
Packit bd1cd8
          f = self.opener.open(req)
Packit bd1cd8
          response = f.read()
Packit bd1cd8
          f.close()
Packit bd1cd8
          return response
Packit bd1cd8
        except urllib2.HTTPError, e:
Packit bd1cd8
          if tries > 3:
Packit bd1cd8
            raise
Packit bd1cd8
          elif e.code == 401:
Packit bd1cd8
            self._Authenticate()
Packit bd1cd8
##           elif e.code >= 500 and e.code < 600:
Packit bd1cd8
##             # Server Error - try again.
Packit bd1cd8
##             continue
Packit bd1cd8
          else:
Packit bd1cd8
            raise
Packit bd1cd8
    finally:
Packit bd1cd8
      socket.setdefaulttimeout(old_timeout)
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class HttpRpcServer(AbstractRpcServer):
Packit bd1cd8
  """Provides a simplified RPC-style interface for HTTP requests."""
Packit bd1cd8
Packit bd1cd8
  def _Authenticate(self):
Packit bd1cd8
    """Save the cookie jar after authentication."""
Packit bd1cd8
    super(HttpRpcServer, self)._Authenticate()
Packit bd1cd8
    if self.save_cookies:
Packit bd1cd8
      StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
Packit bd1cd8
      self.cookie_jar.save()
Packit bd1cd8
Packit bd1cd8
  def _GetOpener(self):
Packit bd1cd8
    """Returns an OpenerDirector that supports cookies and ignores redirects.
Packit bd1cd8
Packit bd1cd8
    Returns:
Packit bd1cd8
      A urllib2.OpenerDirector object.
Packit bd1cd8
    """
Packit bd1cd8
    opener = urllib2.OpenerDirector()
Packit bd1cd8
    opener.add_handler(urllib2.ProxyHandler())
Packit bd1cd8
    opener.add_handler(urllib2.UnknownHandler())
Packit bd1cd8
    opener.add_handler(urllib2.HTTPHandler())
Packit bd1cd8
    opener.add_handler(urllib2.HTTPDefaultErrorHandler())
Packit bd1cd8
    opener.add_handler(urllib2.HTTPSHandler())
Packit bd1cd8
    opener.add_handler(urllib2.HTTPErrorProcessor())
Packit bd1cd8
    if self.save_cookies:
Packit bd1cd8
      self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
Packit bd1cd8
      self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
Packit bd1cd8
      if os.path.exists(self.cookie_file):
Packit bd1cd8
        try:
Packit bd1cd8
          self.cookie_jar.load()
Packit bd1cd8
          self.authenticated = True
Packit bd1cd8
          StatusUpdate("Loaded authentication cookies from %s" %
Packit bd1cd8
                       self.cookie_file)
Packit bd1cd8
        except (cookielib.LoadError, IOError):
Packit bd1cd8
          # Failed to load cookies - just ignore them.
Packit bd1cd8
          pass
Packit bd1cd8
      else:
Packit bd1cd8
        # Create an empty cookie file with mode 600
Packit bd1cd8
        fd = os.open(self.cookie_file, os.O_CREAT, 0600)
Packit bd1cd8
        os.close(fd)
Packit bd1cd8
      # Always chmod the cookie file
Packit bd1cd8
      os.chmod(self.cookie_file, 0600)
Packit bd1cd8
    else:
Packit bd1cd8
      # Don't save cookies across runs of update.py.
Packit bd1cd8
      self.cookie_jar = cookielib.CookieJar()
Packit bd1cd8
    opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
Packit bd1cd8
    return opener
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
Packit bd1cd8
parser.add_option("-y", "--assume_yes", action="store_true",
Packit bd1cd8
                  dest="assume_yes", default=False,
Packit bd1cd8
                  help="Assume that the answer to yes/no questions is 'yes'.")
Packit bd1cd8
# Logging
Packit bd1cd8
group = parser.add_option_group("Logging options")
Packit bd1cd8
group.add_option("-q", "--quiet", action="store_const", const=0,
Packit bd1cd8
                 dest="verbose", help="Print errors only.")
Packit bd1cd8
group.add_option("-v", "--verbose", action="store_const", const=2,
Packit bd1cd8
                 dest="verbose", default=1,
Packit bd1cd8
                 help="Print info level logs (default).")
Packit bd1cd8
group.add_option("--noisy", action="store_const", const=3,
Packit bd1cd8
                 dest="verbose", help="Print all logs.")
Packit bd1cd8
# Review server
Packit bd1cd8
group = parser.add_option_group("Review server options")
Packit bd1cd8
group.add_option("-s", "--server", action="store", dest="server",
Packit bd1cd8
                 default="codereview.appspot.com",
Packit bd1cd8
                 metavar="SERVER",
Packit bd1cd8
                 help=("The server to upload to. The format is host[:port]. "
Packit bd1cd8
                       "Defaults to 'codereview.appspot.com'."))
Packit bd1cd8
group.add_option("-e", "--email", action="store", dest="email",
Packit bd1cd8
                 metavar="EMAIL", default=None,
Packit bd1cd8
                 help="The username to use. Will prompt if omitted.")
Packit bd1cd8
group.add_option("-H", "--host", action="store", dest="host",
Packit bd1cd8
                 metavar="HOST", default=None,
Packit bd1cd8
                 help="Overrides the Host header sent with all RPCs.")
Packit bd1cd8
group.add_option("--no_cookies", action="store_false",
Packit bd1cd8
                 dest="save_cookies", default=True,
Packit bd1cd8
                 help="Do not save authentication cookies to local disk.")
Packit bd1cd8
# Issue
Packit bd1cd8
group = parser.add_option_group("Issue options")
Packit bd1cd8
group.add_option("-d", "--description", action="store", dest="description",
Packit bd1cd8
                 metavar="DESCRIPTION", default=None,
Packit bd1cd8
                 help="Optional description when creating an issue.")
Packit bd1cd8
group.add_option("-f", "--description_file", action="store",
Packit bd1cd8
                 dest="description_file", metavar="DESCRIPTION_FILE",
Packit bd1cd8
                 default=None,
Packit bd1cd8
                 help="Optional path of a file that contains "
Packit bd1cd8
                      "the description when creating an issue.")
Packit bd1cd8
group.add_option("-r", "--reviewers", action="store", dest="reviewers",
Packit bd1cd8
                 metavar="REVIEWERS", default=None,
Packit bd1cd8
                 help="Add reviewers (comma separated email addresses).")
Packit bd1cd8
group.add_option("--cc", action="store", dest="cc",
Packit bd1cd8
                 metavar="CC", default=None,
Packit bd1cd8
                 help="Add CC (comma separated email addresses).")
Packit bd1cd8
# Upload options
Packit bd1cd8
group = parser.add_option_group("Patch options")
Packit bd1cd8
group.add_option("-m", "--message", action="store", dest="message",
Packit bd1cd8
                 metavar="MESSAGE", default=None,
Packit bd1cd8
                 help="A message to identify the patch. "
Packit bd1cd8
                      "Will prompt if omitted.")
Packit bd1cd8
group.add_option("-i", "--issue", type="int", action="store",
Packit bd1cd8
                 metavar="ISSUE", default=None,
Packit bd1cd8
                 help="Issue number to which to add. Defaults to new issue.")
Packit bd1cd8
group.add_option("--download_base", action="store_true",
Packit bd1cd8
                 dest="download_base", default=False,
Packit bd1cd8
                 help="Base files will be downloaded by the server "
Packit bd1cd8
                 "(side-by-side diffs may not work on files with CRs).")
Packit bd1cd8
group.add_option("--rev", action="store", dest="revision",
Packit bd1cd8
                 metavar="REV", default=None,
Packit bd1cd8
                 help="Branch/tree/revision to diff against (used by DVCS).")
Packit bd1cd8
group.add_option("--send_mail", action="store_true",
Packit bd1cd8
                 dest="send_mail", default=False,
Packit bd1cd8
                 help="Send notification email to reviewers.")
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def GetRpcServer(options):
Packit bd1cd8
  """Returns an instance of an AbstractRpcServer.
Packit bd1cd8
Packit bd1cd8
  Returns:
Packit bd1cd8
    A new AbstractRpcServer, on which RPC calls can be made.
Packit bd1cd8
  """
Packit bd1cd8
Packit bd1cd8
  rpc_server_class = HttpRpcServer
Packit bd1cd8
Packit bd1cd8
  def GetUserCredentials():
Packit bd1cd8
    """Prompts the user for a username and password."""
Packit bd1cd8
    email = options.email
Packit bd1cd8
    if email is None:
Packit bd1cd8
      email = GetEmail("Email (login for uploading to %s)" % options.server)
Packit bd1cd8
    password = getpass.getpass("Password for %s: " % email)
Packit bd1cd8
    return (email, password)
Packit bd1cd8
Packit bd1cd8
  # If this is the dev_appserver, use fake authentication.
Packit bd1cd8
  host = (options.host or options.server).lower()
Packit bd1cd8
  if host == "localhost" or host.startswith("localhost:"):
Packit bd1cd8
    email = options.email
Packit bd1cd8
    if email is None:
Packit bd1cd8
      email = "test@example.com"
Packit bd1cd8
      logging.info("Using debug user %s.  Override with --email" % email)
Packit bd1cd8
    server = rpc_server_class(
Packit bd1cd8
        options.server,
Packit bd1cd8
        lambda: (email, "password"),
Packit bd1cd8
        host_override=options.host,
Packit bd1cd8
        extra_headers={"Cookie":
Packit bd1cd8
                       'dev_appserver_login="%s:False"' % email},
Packit bd1cd8
        save_cookies=options.save_cookies)
Packit bd1cd8
    # Don't try to talk to ClientLogin.
Packit bd1cd8
    server.authenticated = True
Packit bd1cd8
    return server
Packit bd1cd8
Packit bd1cd8
  return rpc_server_class(options.server, GetUserCredentials,
Packit bd1cd8
                          host_override=options.host,
Packit bd1cd8
                          save_cookies=options.save_cookies)
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def EncodeMultipartFormData(fields, files):
Packit bd1cd8
  """Encode form fields for multipart/form-data.
Packit bd1cd8
Packit bd1cd8
  Args:
Packit bd1cd8
    fields: A sequence of (name, value) elements for regular form fields.
Packit bd1cd8
    files: A sequence of (name, filename, value) elements for data to be
Packit bd1cd8
           uploaded as files.
Packit bd1cd8
  Returns:
Packit bd1cd8
    (content_type, body) ready for httplib.HTTP instance.
Packit bd1cd8
Packit bd1cd8
  Source:
Packit bd1cd8
    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
Packit bd1cd8
  """
Packit bd1cd8
  BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
Packit bd1cd8
  CRLF = '\r\n'
Packit bd1cd8
  lines = []
Packit bd1cd8
  for (key, value) in fields:
Packit bd1cd8
    lines.append('--' + BOUNDARY)
Packit bd1cd8
    lines.append('Content-Disposition: form-data; name="%s"' % key)
Packit bd1cd8
    lines.append('')
Packit bd1cd8
    lines.append(value)
Packit bd1cd8
  for (key, filename, value) in files:
Packit bd1cd8
    lines.append('--' + BOUNDARY)
Packit bd1cd8
    lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
Packit bd1cd8
             (key, filename))
Packit bd1cd8
    lines.append('Content-Type: %s' % GetContentType(filename))
Packit bd1cd8
    lines.append('')
Packit bd1cd8
    lines.append(value)
Packit bd1cd8
  lines.append('--' + BOUNDARY + '--')
Packit bd1cd8
  lines.append('')
Packit bd1cd8
  body = CRLF.join(lines)
Packit bd1cd8
  content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
Packit bd1cd8
  return content_type, body
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def GetContentType(filename):
Packit bd1cd8
  """Helper to guess the content-type from the filename."""
Packit bd1cd8
  return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
# Use a shell for subcommands on Windows to get a PATH search.
Packit bd1cd8
use_shell = sys.platform.startswith("win")
Packit bd1cd8
Packit bd1cd8
def RunShellWithReturnCode(command, print_output=False,
Packit bd1cd8
                           universal_newlines=True):
Packit bd1cd8
  """Executes a command and returns the output from stdout and the return code.
Packit bd1cd8
Packit bd1cd8
  Args:
Packit bd1cd8
    command: Command to execute.
Packit bd1cd8
    print_output: If True, the output is printed to stdout.
Packit bd1cd8
                  If False, both stdout and stderr are ignored.
Packit bd1cd8
    universal_newlines: Use universal_newlines flag (default: True).
Packit bd1cd8
Packit bd1cd8
  Returns:
Packit bd1cd8
    Tuple (output, return code)
Packit bd1cd8
  """
Packit bd1cd8
  logging.info("Running %s", command)
Packit bd1cd8
  p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Packit bd1cd8
                       shell=use_shell, universal_newlines=universal_newlines)
Packit bd1cd8
  if print_output:
Packit bd1cd8
    output_array = []
Packit bd1cd8
    while True:
Packit bd1cd8
      line = p.stdout.readline()
Packit bd1cd8
      if not line:
Packit bd1cd8
        break
Packit bd1cd8
      print line.strip("\n")
Packit bd1cd8
      output_array.append(line)
Packit bd1cd8
    output = "".join(output_array)
Packit bd1cd8
  else:
Packit bd1cd8
    output = p.stdout.read()
Packit bd1cd8
  p.wait()
Packit bd1cd8
  errout = p.stderr.read()
Packit bd1cd8
  if print_output and errout:
Packit bd1cd8
    print >>sys.stderr, errout
Packit bd1cd8
  p.stdout.close()
Packit bd1cd8
  p.stderr.close()
Packit bd1cd8
  return output, p.returncode
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def RunShell(command, silent_ok=False, universal_newlines=True,
Packit bd1cd8
             print_output=False):
Packit bd1cd8
  data, retcode = RunShellWithReturnCode(command, print_output,
Packit bd1cd8
                                         universal_newlines)
Packit bd1cd8
  if retcode:
Packit bd1cd8
    ErrorExit("Got error status from %s:\n%s" % (command, data))
Packit bd1cd8
  if not silent_ok and not data:
Packit bd1cd8
    ErrorExit("No output from %s" % command)
Packit bd1cd8
  return data
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class VersionControlSystem(object):
Packit bd1cd8
  """Abstract base class providing an interface to the VCS."""
Packit bd1cd8
Packit bd1cd8
  def __init__(self, options):
Packit bd1cd8
    """Constructor.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      options: Command line options.
Packit bd1cd8
    """
Packit bd1cd8
    self.options = options
Packit bd1cd8
Packit bd1cd8
  def GenerateDiff(self, args):
Packit bd1cd8
    """Return the current diff as a string.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      args: Extra arguments to pass to the diff command.
Packit bd1cd8
    """
Packit bd1cd8
    raise NotImplementedError(
Packit bd1cd8
        "abstract method -- subclass %s must override" % self.__class__)
Packit bd1cd8
Packit bd1cd8
  def GetUnknownFiles(self):
Packit bd1cd8
    """Return a list of files unknown to the VCS."""
Packit bd1cd8
    raise NotImplementedError(
Packit bd1cd8
        "abstract method -- subclass %s must override" % self.__class__)
Packit bd1cd8
Packit bd1cd8
  def CheckForUnknownFiles(self):
Packit bd1cd8
    """Show an "are you sure?" prompt if there are unknown files."""
Packit bd1cd8
    unknown_files = self.GetUnknownFiles()
Packit bd1cd8
    if unknown_files:
Packit bd1cd8
      print "The following files are not added to version control:"
Packit bd1cd8
      for line in unknown_files:
Packit bd1cd8
        print line
Packit bd1cd8
      prompt = "Are you sure to continue?(y/N) "
Packit bd1cd8
      answer = raw_input(prompt).strip()
Packit bd1cd8
      if answer != "y":
Packit bd1cd8
        ErrorExit("User aborted")
Packit bd1cd8
Packit bd1cd8
  def GetBaseFile(self, filename):
Packit bd1cd8
    """Get the content of the upstream version of a file.
Packit bd1cd8
Packit bd1cd8
    Returns:
Packit bd1cd8
      A tuple (base_content, new_content, is_binary, status)
Packit bd1cd8
        base_content: The contents of the base file.
Packit bd1cd8
        new_content: For text files, this is empty.  For binary files, this is
Packit bd1cd8
          the contents of the new file, since the diff output won't contain
Packit bd1cd8
          information to reconstruct the current file.
Packit bd1cd8
        is_binary: True iff the file is binary.
Packit bd1cd8
        status: The status of the file.
Packit bd1cd8
    """
Packit bd1cd8
Packit bd1cd8
    raise NotImplementedError(
Packit bd1cd8
        "abstract method -- subclass %s must override" % self.__class__)
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
  def GetBaseFiles(self, diff):
Packit bd1cd8
    """Helper that calls GetBase file for each file in the patch.
Packit bd1cd8
Packit bd1cd8
    Returns:
Packit bd1cd8
      A dictionary that maps from filename to GetBaseFile's tuple.  Filenames
Packit bd1cd8
      are retrieved based on lines that start with "Index:" or
Packit bd1cd8
      "Property changes on:".
Packit bd1cd8
    """
Packit bd1cd8
    files = {}
Packit bd1cd8
    for line in diff.splitlines(True):
Packit bd1cd8
      if line.startswith('Index:') or line.startswith('Property changes on:'):
Packit bd1cd8
        unused, filename = line.split(':', 1)
Packit bd1cd8
        # On Windows if a file has property changes its filename uses '\'
Packit bd1cd8
        # instead of '/'.
Packit bd1cd8
        filename = filename.strip().replace('\\', '/')
Packit bd1cd8
        files[filename] = self.GetBaseFile(filename)
Packit bd1cd8
    return files
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
  def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
Packit bd1cd8
                      files):
Packit bd1cd8
    """Uploads the base files (and if necessary, the current ones as well)."""
Packit bd1cd8
Packit bd1cd8
    def UploadFile(filename, file_id, content, is_binary, status, is_base):
Packit bd1cd8
      """Uploads a file to the server."""
Packit bd1cd8
      file_too_large = False
Packit bd1cd8
      if is_base:
Packit bd1cd8
        type = "base"
Packit bd1cd8
      else:
Packit bd1cd8
        type = "current"
Packit bd1cd8
      if len(content) > MAX_UPLOAD_SIZE:
Packit bd1cd8
        print ("Not uploading the %s file for %s because it's too large." %
Packit bd1cd8
               (type, filename))
Packit bd1cd8
        file_too_large = True
Packit bd1cd8
        content = ""
Packit bd1cd8
      checksum = md5.new(content).hexdigest()
Packit bd1cd8
      if options.verbose > 0 and not file_too_large:
Packit bd1cd8
        print "Uploading %s file for %s" % (type, filename)
Packit bd1cd8
      url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
Packit bd1cd8
      form_fields = [("filename", filename),
Packit bd1cd8
                     ("status", status),
Packit bd1cd8
                     ("checksum", checksum),
Packit bd1cd8
                     ("is_binary", str(is_binary)),
Packit bd1cd8
                     ("is_current", str(not is_base)),
Packit bd1cd8
                    ]
Packit bd1cd8
      if file_too_large:
Packit bd1cd8
        form_fields.append(("file_too_large", "1"))
Packit bd1cd8
      if options.email:
Packit bd1cd8
        form_fields.append(("user", options.email))
Packit bd1cd8
      ctype, body = EncodeMultipartFormData(form_fields,
Packit bd1cd8
                                            [("data", filename, content)])
Packit bd1cd8
      response_body = rpc_server.Send(url, body,
Packit bd1cd8
                                      content_type=ctype)
Packit bd1cd8
      if not response_body.startswith("OK"):
Packit bd1cd8
        StatusUpdate("  --> %s" % response_body)
Packit bd1cd8
        sys.exit(1)
Packit bd1cd8
Packit bd1cd8
    patches = dict()
Packit bd1cd8
    [patches.setdefault(v, k) for k, v in patch_list]
Packit bd1cd8
    for filename in patches.keys():
Packit bd1cd8
      base_content, new_content, is_binary, status = files[filename]
Packit bd1cd8
      file_id_str = patches.get(filename)
Packit bd1cd8
      if file_id_str.find("nobase") != -1:
Packit bd1cd8
        base_content = None
Packit bd1cd8
        file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
Packit bd1cd8
      file_id = int(file_id_str)
Packit bd1cd8
      if base_content != None:
Packit bd1cd8
        UploadFile(filename, file_id, base_content, is_binary, status, True)
Packit bd1cd8
      if new_content != None:
Packit bd1cd8
        UploadFile(filename, file_id, new_content, is_binary, status, False)
Packit bd1cd8
Packit bd1cd8
  def IsImage(self, filename):
Packit bd1cd8
    """Returns true if the filename has an image extension."""
Packit bd1cd8
    mimetype =  mimetypes.guess_type(filename)[0]
Packit bd1cd8
    if not mimetype:
Packit bd1cd8
      return False
Packit bd1cd8
    return mimetype.startswith("image/")
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class SubversionVCS(VersionControlSystem):
Packit bd1cd8
  """Implementation of the VersionControlSystem interface for Subversion."""
Packit bd1cd8
Packit bd1cd8
  def __init__(self, options):
Packit bd1cd8
    super(SubversionVCS, self).__init__(options)
Packit bd1cd8
    if self.options.revision:
Packit bd1cd8
      match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
Packit bd1cd8
      if not match:
Packit bd1cd8
        ErrorExit("Invalid Subversion revision %s." % self.options.revision)
Packit bd1cd8
      self.rev_start = match.group(1)
Packit bd1cd8
      self.rev_end = match.group(3)
Packit bd1cd8
    else:
Packit bd1cd8
      self.rev_start = self.rev_end = None
Packit bd1cd8
    # Cache output from "svn list -r REVNO dirname".
Packit bd1cd8
    # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
Packit bd1cd8
    self.svnls_cache = {}
Packit bd1cd8
    # SVN base URL is required to fetch files deleted in an older revision.
Packit bd1cd8
    # Result is cached to not guess it over and over again in GetBaseFile().
Packit bd1cd8
    required = self.options.download_base or self.options.revision is not None
Packit bd1cd8
    self.svn_base = self._GuessBase(required)
Packit bd1cd8
Packit bd1cd8
  def GuessBase(self, required):
Packit bd1cd8
    """Wrapper for _GuessBase."""
Packit bd1cd8
    return self.svn_base
Packit bd1cd8
Packit bd1cd8
  def _GuessBase(self, required):
Packit bd1cd8
    """Returns the SVN base URL.
Packit bd1cd8
Packit bd1cd8
    Args:
Packit bd1cd8
      required: If true, exits if the url can't be guessed, otherwise None is
Packit bd1cd8
        returned.
Packit bd1cd8
    """
Packit bd1cd8
    info = RunShell(["svn", "info"])
Packit bd1cd8
    for line in info.splitlines():
Packit bd1cd8
      words = line.split()
Packit bd1cd8
      if len(words) == 2 and words[0] == "URL:":
Packit bd1cd8
        url = words[1]
Packit bd1cd8
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
Packit bd1cd8
        username, netloc = urllib.splituser(netloc)
Packit bd1cd8
        if username:
Packit bd1cd8
          logging.info("Removed username from base URL")
Packit bd1cd8
        if netloc.endswith("svn.python.org"):
Packit bd1cd8
          if netloc == "svn.python.org":
Packit bd1cd8
            if path.startswith("/projects/"):
Packit bd1cd8
              path = path[9:]
Packit bd1cd8
          elif netloc != "pythondev@svn.python.org":
Packit bd1cd8
            ErrorExit("Unrecognized Python URL: %s" % url)
Packit bd1cd8
          base = "http://svn.python.org/view/*checkout*%s/" % path
Packit bd1cd8
          logging.info("Guessed Python base = %s", base)
Packit bd1cd8
        elif netloc.endswith("svn.collab.net"):
Packit bd1cd8
          if path.startswith("/repos/"):
Packit bd1cd8
            path = path[6:]
Packit bd1cd8
          base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
Packit bd1cd8
          logging.info("Guessed CollabNet base = %s", base)
Packit bd1cd8
        elif netloc.endswith(".googlecode.com"):
Packit bd1cd8
          path = path + "/"
Packit bd1cd8
          base = urlparse.urlunparse(("http", netloc, path, params,
Packit bd1cd8
                                      query, fragment))
Packit bd1cd8
          logging.info("Guessed Google Code base = %s", base)
Packit bd1cd8
        else:
Packit bd1cd8
          path = path + "/"
Packit bd1cd8
          base = urlparse.urlunparse((scheme, netloc, path, params,
Packit bd1cd8
                                      query, fragment))
Packit bd1cd8
          logging.info("Guessed base = %s", base)
Packit bd1cd8
        return base
Packit bd1cd8
    if required:
Packit bd1cd8
      ErrorExit("Can't find URL in output from svn info")
Packit bd1cd8
    return None
Packit bd1cd8
Packit bd1cd8
  def GenerateDiff(self, args):
Packit bd1cd8
    cmd = ["svn", "diff"]
Packit bd1cd8
    if self.options.revision:
Packit bd1cd8
      cmd += ["-r", self.options.revision]
Packit bd1cd8
    cmd.extend(args)
Packit bd1cd8
    data = RunShell(cmd)
Packit bd1cd8
    count = 0
Packit bd1cd8
    for line in data.splitlines():
Packit bd1cd8
      if line.startswith("Index:") or line.startswith("Property changes on:"):
Packit bd1cd8
        count += 1
Packit bd1cd8
        logging.info(line)
Packit bd1cd8
    if not count:
Packit bd1cd8
      ErrorExit("No valid patches found in output from svn diff")
Packit bd1cd8
    return data
Packit bd1cd8
Packit bd1cd8
  def _CollapseKeywords(self, content, keyword_str):
Packit bd1cd8
    """Collapses SVN keywords."""
Packit bd1cd8
    # svn cat translates keywords but svn diff doesn't. As a result of this
Packit bd1cd8
    # behavior patching.PatchChunks() fails with a chunk mismatch error.
Packit bd1cd8
    # This part was originally written by the Review Board development team
Packit bd1cd8
    # who had the same problem (http://reviews.review-board.org/r/276/).
Packit bd1cd8
    # Mapping of keywords to known aliases
Packit bd1cd8
    svn_keywords = {
Packit bd1cd8
      # Standard keywords
Packit bd1cd8
      'Date':                ['Date', 'LastChangedDate'],
Packit bd1cd8
      'Revision':            ['Revision', 'LastChangedRevision', 'Rev'],
Packit bd1cd8
      'Author':              ['Author', 'LastChangedBy'],
Packit bd1cd8
      'HeadURL':             ['HeadURL', 'URL'],
Packit bd1cd8
      'Id':                  ['Id'],
Packit bd1cd8
Packit bd1cd8
      # Aliases
Packit bd1cd8
      'LastChangedDate':     ['LastChangedDate', 'Date'],
Packit bd1cd8
      'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
Packit bd1cd8
      'LastChangedBy':       ['LastChangedBy', 'Author'],
Packit bd1cd8
      'URL':                 ['URL', 'HeadURL'],
Packit bd1cd8
    }
Packit bd1cd8
Packit bd1cd8
    def repl(m):
Packit bd1cd8
       if m.group(2):
Packit bd1cd8
         return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
Packit bd1cd8
       return "$%s$" % m.group(1)
Packit bd1cd8
    keywords = [keyword
Packit bd1cd8
                for name in keyword_str.split(" ")
Packit bd1cd8
                for keyword in svn_keywords.get(name, [])]
Packit bd1cd8
    return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
Packit bd1cd8
Packit bd1cd8
  def GetUnknownFiles(self):
Packit bd1cd8
    status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
Packit bd1cd8
    unknown_files = []
Packit bd1cd8
    for line in status.split("\n"):
Packit bd1cd8
      if line and line[0] == "?":
Packit bd1cd8
        unknown_files.append(line)
Packit bd1cd8
    return unknown_files
Packit bd1cd8
Packit bd1cd8
  def ReadFile(self, filename):
Packit bd1cd8
    """Returns the contents of a file."""
Packit bd1cd8
    file = open(filename, 'rb')
Packit bd1cd8
    result = ""
Packit bd1cd8
    try:
Packit bd1cd8
      result = file.read()
Packit bd1cd8
    finally:
Packit bd1cd8
      file.close()
Packit bd1cd8
    return result
Packit bd1cd8
Packit bd1cd8
  def GetStatus(self, filename):
Packit bd1cd8
    """Returns the status of a file."""
Packit bd1cd8
    if not self.options.revision:
Packit bd1cd8
      status = RunShell(["svn", "status", "--ignore-externals", filename])
Packit bd1cd8
      if not status:
Packit bd1cd8
        ErrorExit("svn status returned no output for %s" % filename)
Packit bd1cd8
      status_lines = status.splitlines()
Packit bd1cd8
      # If file is in a cl, the output will begin with
Packit bd1cd8
      # "\n--- Changelist 'cl_name':\n".  See
Packit bd1cd8
      # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
Packit bd1cd8
      if (len(status_lines) == 3 and
Packit bd1cd8
          not status_lines[0] and
Packit bd1cd8
          status_lines[1].startswith("--- Changelist")):
Packit bd1cd8
        status = status_lines[2]
Packit bd1cd8
      else:
Packit bd1cd8
        status = status_lines[0]
Packit bd1cd8
    # If we have a revision to diff against we need to run "svn list"
Packit bd1cd8
    # for the old and the new revision and compare the results to get
Packit bd1cd8
    # the correct status for a file.
Packit bd1cd8
    else:
Packit bd1cd8
      dirname, relfilename = os.path.split(filename)
Packit bd1cd8
      if dirname not in self.svnls_cache:
Packit bd1cd8
        cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
Packit bd1cd8
        out, returncode = RunShellWithReturnCode(cmd)
Packit bd1cd8
        if returncode:
Packit bd1cd8
          ErrorExit("Failed to get status for %s." % filename)
Packit bd1cd8
        old_files = out.splitlines()
Packit bd1cd8
        args = ["svn", "list"]
Packit bd1cd8
        if self.rev_end:
Packit bd1cd8
          args += ["-r", self.rev_end]
Packit bd1cd8
        cmd = args + [dirname or "."]
Packit bd1cd8
        out, returncode = RunShellWithReturnCode(cmd)
Packit bd1cd8
        if returncode:
Packit bd1cd8
          ErrorExit("Failed to run command %s" % cmd)
Packit bd1cd8
        self.svnls_cache[dirname] = (old_files, out.splitlines())
Packit bd1cd8
      old_files, new_files = self.svnls_cache[dirname]
Packit bd1cd8
      if relfilename in old_files and relfilename not in new_files:
Packit bd1cd8
        status = "D   "
Packit bd1cd8
      elif relfilename in old_files and relfilename in new_files:
Packit bd1cd8
        status = "M   "
Packit bd1cd8
      else:
Packit bd1cd8
        status = "A   "
Packit bd1cd8
    return status
Packit bd1cd8
Packit bd1cd8
  def GetBaseFile(self, filename):
Packit bd1cd8
    status = self.GetStatus(filename)
Packit bd1cd8
    base_content = None
Packit bd1cd8
    new_content = None
Packit bd1cd8
Packit bd1cd8
    # If a file is copied its status will be "A  +", which signifies
Packit bd1cd8
    # "addition-with-history".  See "svn st" for more information.  We need to
Packit bd1cd8
    # upload the original file or else diff parsing will fail if the file was
Packit bd1cd8
    # edited.
Packit bd1cd8
    if status[0] == "A" and status[3] != "+":
Packit bd1cd8
      # We'll need to upload the new content if we're adding a binary file
Packit bd1cd8
      # since diff's output won't contain it.
Packit bd1cd8
      mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
Packit bd1cd8
                          silent_ok=True)
Packit bd1cd8
      base_content = ""
Packit bd1cd8
      is_binary = mimetype and not mimetype.startswith("text/")
Packit bd1cd8
      if is_binary and self.IsImage(filename):
Packit bd1cd8
        new_content = self.ReadFile(filename)
Packit bd1cd8
    elif (status[0] in ("M", "D", "R") or
Packit bd1cd8
          (status[0] == "A" and status[3] == "+") or  # Copied file.
Packit bd1cd8
          (status[0] == " " and status[1] == "M")):  # Property change.
Packit bd1cd8
      args = []
Packit bd1cd8
      if self.options.revision:
Packit bd1cd8
        url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
Packit bd1cd8
      else:
Packit bd1cd8
        # Don't change filename, it's needed later.
Packit bd1cd8
        url = filename
Packit bd1cd8
        args += ["-r", "BASE"]
Packit bd1cd8
      cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
Packit bd1cd8
      mimetype, returncode = RunShellWithReturnCode(cmd)
Packit bd1cd8
      if returncode:
Packit bd1cd8
        # File does not exist in the requested revision.
Packit bd1cd8
        # Reset mimetype, it contains an error message.
Packit bd1cd8
        mimetype = ""
Packit bd1cd8
      get_base = False
Packit bd1cd8
      is_binary = mimetype and not mimetype.startswith("text/")
Packit bd1cd8
      if status[0] == " ":
Packit bd1cd8
        # Empty base content just to force an upload.
Packit bd1cd8
        base_content = ""
Packit bd1cd8
      elif is_binary:
Packit bd1cd8
        if self.IsImage(filename):
Packit bd1cd8
          get_base = True
Packit bd1cd8
          if status[0] == "M":
Packit bd1cd8
            if not self.rev_end:
Packit bd1cd8
              new_content = self.ReadFile(filename)
Packit bd1cd8
            else:
Packit bd1cd8
              url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
Packit bd1cd8
              new_content = RunShell(["svn", "cat", url],
Packit bd1cd8
                                     universal_newlines=True, silent_ok=True)
Packit bd1cd8
        else:
Packit bd1cd8
          base_content = ""
Packit bd1cd8
      else:
Packit bd1cd8
        get_base = True
Packit bd1cd8
Packit bd1cd8
      if get_base:
Packit bd1cd8
        if is_binary:
Packit bd1cd8
          universal_newlines = False
Packit bd1cd8
        else:
Packit bd1cd8
          universal_newlines = True
Packit bd1cd8
        if self.rev_start:
Packit bd1cd8
          # "svn cat -r REV delete_file.txt" doesn't work. cat requires
Packit bd1cd8
          # the full URL with "@REV" appended instead of using "-r" option.
Packit bd1cd8
          url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
Packit bd1cd8
          base_content = RunShell(["svn", "cat", url],
Packit bd1cd8
                                  universal_newlines=universal_newlines,
Packit bd1cd8
                                  silent_ok=True)
Packit bd1cd8
        else:
Packit bd1cd8
          base_content = RunShell(["svn", "cat", filename],
Packit bd1cd8
                                  universal_newlines=universal_newlines,
Packit bd1cd8
                                  silent_ok=True)
Packit bd1cd8
        if not is_binary:
Packit bd1cd8
          args = []
Packit bd1cd8
          if self.rev_start:
Packit bd1cd8
            url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
Packit bd1cd8
          else:
Packit bd1cd8
            url = filename
Packit bd1cd8
            args += ["-r", "BASE"]
Packit bd1cd8
          cmd = ["svn"] + args + ["propget", "svn:keywords", url]
Packit bd1cd8
          keywords, returncode = RunShellWithReturnCode(cmd)
Packit bd1cd8
          if keywords and not returncode:
Packit bd1cd8
            base_content = self._CollapseKeywords(base_content, keywords)
Packit bd1cd8
    else:
Packit bd1cd8
      StatusUpdate("svn status returned unexpected output: %s" % status)
Packit bd1cd8
      sys.exit(1)
Packit bd1cd8
    return base_content, new_content, is_binary, status[0:5]
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class GitVCS(VersionControlSystem):
Packit bd1cd8
  """Implementation of the VersionControlSystem interface for Git."""
Packit bd1cd8
Packit bd1cd8
  def __init__(self, options):
Packit bd1cd8
    super(GitVCS, self).__init__(options)
Packit bd1cd8
    # Map of filename -> hash of base file.
Packit bd1cd8
    self.base_hashes = {}
Packit bd1cd8
Packit bd1cd8
  def GenerateDiff(self, extra_args):
Packit bd1cd8
    # This is more complicated than svn's GenerateDiff because we must convert
Packit bd1cd8
    # the diff output to include an svn-style "Index:" line as well as record
Packit bd1cd8
    # the hashes of the base files, so we can upload them along with our diff.
Packit bd1cd8
    if self.options.revision:
Packit bd1cd8
      extra_args = [self.options.revision] + extra_args
Packit bd1cd8
    gitdiff = RunShell(["git", "diff", "--full-index"] + extra_args)
Packit bd1cd8
    svndiff = []
Packit bd1cd8
    filecount = 0
Packit bd1cd8
    filename = None
Packit bd1cd8
    for line in gitdiff.splitlines():
Packit bd1cd8
      match = re.match(r"diff --git a/(.*) b/.*$", line)
Packit bd1cd8
      if match:
Packit bd1cd8
        filecount += 1
Packit bd1cd8
        filename = match.group(1)
Packit bd1cd8
        svndiff.append("Index: %s\n" % filename)
Packit bd1cd8
      else:
Packit bd1cd8
        # The "index" line in a git diff looks like this (long hashes elided):
Packit bd1cd8
        #   index 82c0d44..b2cee3f 100755
Packit bd1cd8
        # We want to save the left hash, as that identifies the base file.
Packit bd1cd8
        match = re.match(r"index (\w+)\.\.", line)
Packit bd1cd8
        if match:
Packit bd1cd8
          self.base_hashes[filename] = match.group(1)
Packit bd1cd8
      svndiff.append(line + "\n")
Packit bd1cd8
    if not filecount:
Packit bd1cd8
      ErrorExit("No valid patches found in output from git diff")
Packit bd1cd8
    return "".join(svndiff)
Packit bd1cd8
Packit bd1cd8
  def GetUnknownFiles(self):
Packit bd1cd8
    status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
Packit bd1cd8
                      silent_ok=True)
Packit bd1cd8
    return status.splitlines()
Packit bd1cd8
Packit bd1cd8
  def GetBaseFile(self, filename):
Packit bd1cd8
    hash = self.base_hashes[filename]
Packit bd1cd8
    base_content = None
Packit bd1cd8
    new_content = None
Packit bd1cd8
    is_binary = False
Packit bd1cd8
    if hash == "0" * 40:  # All-zero hash indicates no base file.
Packit bd1cd8
      status = "A"
Packit bd1cd8
      base_content = ""
Packit bd1cd8
    else:
Packit bd1cd8
      status = "M"
Packit bd1cd8
      base_content, returncode = RunShellWithReturnCode(["git", "show", hash])
Packit bd1cd8
      if returncode:
Packit bd1cd8
        ErrorExit("Got error status from 'git show %s'" % hash)
Packit bd1cd8
    return (base_content, new_content, is_binary, status)
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
class MercurialVCS(VersionControlSystem):
Packit bd1cd8
  """Implementation of the VersionControlSystem interface for Mercurial."""
Packit bd1cd8
Packit bd1cd8
  def __init__(self, options, repo_dir):
Packit bd1cd8
    super(MercurialVCS, self).__init__(options)
Packit bd1cd8
    # Absolute path to repository (we can be in a subdir)
Packit bd1cd8
    self.repo_dir = os.path.normpath(repo_dir)
Packit bd1cd8
    # Compute the subdir
Packit bd1cd8
    cwd = os.path.normpath(os.getcwd())
Packit bd1cd8
    assert cwd.startswith(self.repo_dir)
Packit bd1cd8
    self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
Packit bd1cd8
    if self.options.revision:
Packit bd1cd8
      self.base_rev = self.options.revision
Packit bd1cd8
    else:
Packit bd1cd8
      self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
Packit bd1cd8
Packit bd1cd8
  def _GetRelPath(self, filename):
Packit bd1cd8
    """Get relative path of a file according to the current directory,
Packit bd1cd8
    given its logical path in the repo."""
Packit bd1cd8
    assert filename.startswith(self.subdir), filename
Packit bd1cd8
    return filename[len(self.subdir):].lstrip(r"\/")
Packit bd1cd8
Packit bd1cd8
  def GenerateDiff(self, extra_args):
Packit bd1cd8
    # If no file specified, restrict to the current subdir
Packit bd1cd8
    extra_args = extra_args or ["."]
Packit bd1cd8
    cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
Packit bd1cd8
    data = RunShell(cmd, silent_ok=True)
Packit bd1cd8
    svndiff = []
Packit bd1cd8
    filecount = 0
Packit bd1cd8
    for line in data.splitlines():
Packit bd1cd8
      m = re.match("diff --git a/(\S+) b/(\S+)", line)
Packit bd1cd8
      if m:
Packit bd1cd8
        # Modify line to make it look like as it comes from svn diff.
Packit bd1cd8
        # With this modification no changes on the server side are required
Packit bd1cd8
        # to make upload.py work with Mercurial repos.
Packit bd1cd8
        # NOTE: for proper handling of moved/copied files, we have to use
Packit bd1cd8
        # the second filename.
Packit bd1cd8
        filename = m.group(2)
Packit bd1cd8
        svndiff.append("Index: %s" % filename)
Packit bd1cd8
        svndiff.append("=" * 67)
Packit bd1cd8
        filecount += 1
Packit bd1cd8
        logging.info(line)
Packit bd1cd8
      else:
Packit bd1cd8
        svndiff.append(line)
Packit bd1cd8
    if not filecount:
Packit bd1cd8
      ErrorExit("No valid patches found in output from hg diff")
Packit bd1cd8
    return "\n".join(svndiff) + "\n"
Packit bd1cd8
Packit bd1cd8
  def GetUnknownFiles(self):
Packit bd1cd8
    """Return a list of files unknown to the VCS."""
Packit bd1cd8
    args = []
Packit bd1cd8
    status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
Packit bd1cd8
        silent_ok=True)
Packit bd1cd8
    unknown_files = []
Packit bd1cd8
    for line in status.splitlines():
Packit bd1cd8
      st, fn = line.split(" ", 1)
Packit bd1cd8
      if st == "?":
Packit bd1cd8
        unknown_files.append(fn)
Packit bd1cd8
    return unknown_files
Packit bd1cd8
Packit bd1cd8
  def GetBaseFile(self, filename):
Packit bd1cd8
    # "hg status" and "hg cat" both take a path relative to the current subdir
Packit bd1cd8
    # rather than to the repo root, but "hg diff" has given us the full path
Packit bd1cd8
    # to the repo root.
Packit bd1cd8
    base_content = ""
Packit bd1cd8
    new_content = None
Packit bd1cd8
    is_binary = False
Packit bd1cd8
    oldrelpath = relpath = self._GetRelPath(filename)
Packit bd1cd8
    # "hg status -C" returns two lines for moved/copied files, one otherwise
Packit bd1cd8
    out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
Packit bd1cd8
    out = out.splitlines()
Packit bd1cd8
    # HACK: strip error message about missing file/directory if it isn't in
Packit bd1cd8
    # the working copy
Packit bd1cd8
    if out[0].startswith('%s: ' % relpath):
Packit bd1cd8
      out = out[1:]
Packit bd1cd8
    if len(out) > 1:
Packit bd1cd8
      # Moved/copied => considered as modified, use old filename to
Packit bd1cd8
      # retrieve base contents
Packit bd1cd8
      oldrelpath = out[1].strip()
Packit bd1cd8
      status = "M"
Packit bd1cd8
    else:
Packit bd1cd8
      status, _ = out[0].split(' ', 1)
Packit bd1cd8
    if status != "A":
Packit bd1cd8
      base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
Packit bd1cd8
        silent_ok=True)
Packit bd1cd8
      is_binary = "\0" in base_content  # Mercurial's heuristic
Packit bd1cd8
    if status != "R":
Packit bd1cd8
      new_content = open(relpath, "rb").read()
Packit bd1cd8
      is_binary = is_binary or "\0" in new_content
Packit bd1cd8
    if is_binary and base_content:
Packit bd1cd8
      # Fetch again without converting newlines
Packit bd1cd8
      base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
Packit bd1cd8
        silent_ok=True, universal_newlines=False)
Packit bd1cd8
    if not is_binary or not self.IsImage(relpath):
Packit bd1cd8
      new_content = None
Packit bd1cd8
    return base_content, new_content, is_binary, status
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
# NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
Packit bd1cd8
def SplitPatch(data):
Packit bd1cd8
  """Splits a patch into separate pieces for each file.
Packit bd1cd8
Packit bd1cd8
  Args:
Packit bd1cd8
    data: A string containing the output of svn diff.
Packit bd1cd8
Packit bd1cd8
  Returns:
Packit bd1cd8
    A list of 2-tuple (filename, text) where text is the svn diff output
Packit bd1cd8
      pertaining to filename.
Packit bd1cd8
  """
Packit bd1cd8
  patches = []
Packit bd1cd8
  filename = None
Packit bd1cd8
  diff = []
Packit bd1cd8
  for line in data.splitlines(True):
Packit bd1cd8
    new_filename = None
Packit bd1cd8
    if line.startswith('Index:'):
Packit bd1cd8
      unused, new_filename = line.split(':', 1)
Packit bd1cd8
      new_filename = new_filename.strip()
Packit bd1cd8
    elif line.startswith('Property changes on:'):
Packit bd1cd8
      unused, temp_filename = line.split(':', 1)
Packit bd1cd8
      # When a file is modified, paths use '/' between directories, however
Packit bd1cd8
      # when a property is modified '\' is used on Windows.  Make them the same
Packit bd1cd8
      # otherwise the file shows up twice.
Packit bd1cd8
      temp_filename = temp_filename.strip().replace('\\', '/')
Packit bd1cd8
      if temp_filename != filename:
Packit bd1cd8
        # File has property changes but no modifications, create a new diff.
Packit bd1cd8
        new_filename = temp_filename
Packit bd1cd8
    if new_filename:
Packit bd1cd8
      if filename and diff:
Packit bd1cd8
        patches.append((filename, ''.join(diff)))
Packit bd1cd8
      filename = new_filename
Packit bd1cd8
      diff = [line]
Packit bd1cd8
      continue
Packit bd1cd8
    if diff is not None:
Packit bd1cd8
      diff.append(line)
Packit bd1cd8
  if filename and diff:
Packit bd1cd8
    patches.append((filename, ''.join(diff)))
Packit bd1cd8
  return patches
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
Packit bd1cd8
  """Uploads a separate patch for each file in the diff output.
Packit bd1cd8
Packit bd1cd8
  Returns a list of [patch_key, filename] for each file.
Packit bd1cd8
  """
Packit bd1cd8
  patches = SplitPatch(data)
Packit bd1cd8
  rv = []
Packit bd1cd8
  for patch in patches:
Packit bd1cd8
    if len(patch[1]) > MAX_UPLOAD_SIZE:
Packit bd1cd8
      print ("Not uploading the patch for " + patch[0] +
Packit bd1cd8
             " because the file is too large.")
Packit bd1cd8
      continue
Packit bd1cd8
    form_fields = [("filename", patch[0])]
Packit bd1cd8
    if not options.download_base:
Packit bd1cd8
      form_fields.append(("content_upload", "1"))
Packit bd1cd8
    files = [("data", "data.diff", patch[1])]
Packit bd1cd8
    ctype, body = EncodeMultipartFormData(form_fields, files)
Packit bd1cd8
    url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
Packit bd1cd8
    print "Uploading patch for " + patch[0]
Packit bd1cd8
    response_body = rpc_server.Send(url, body, content_type=ctype)
Packit bd1cd8
    lines = response_body.splitlines()
Packit bd1cd8
    if not lines or lines[0] != "OK":
Packit bd1cd8
      StatusUpdate("  --> %s" % response_body)
Packit bd1cd8
      sys.exit(1)
Packit bd1cd8
    rv.append([lines[1], patch[0]])
Packit bd1cd8
  return rv
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def GuessVCS(options):
Packit bd1cd8
  """Helper to guess the version control system.
Packit bd1cd8
Packit bd1cd8
  This examines the current directory, guesses which VersionControlSystem
Packit bd1cd8
  we're using, and returns an instance of the appropriate class.  Exit with an
Packit bd1cd8
  error if we can't figure it out.
Packit bd1cd8
Packit bd1cd8
  Returns:
Packit bd1cd8
    A VersionControlSystem instance. Exits if the VCS can't be guessed.
Packit bd1cd8
  """
Packit bd1cd8
  # Mercurial has a command to get the base directory of a repository
Packit bd1cd8
  # Try running it, but don't die if we don't have hg installed.
Packit bd1cd8
  # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
Packit bd1cd8
  try:
Packit bd1cd8
    out, returncode = RunShellWithReturnCode(["hg", "root"])
Packit bd1cd8
    if returncode == 0:
Packit bd1cd8
      return MercurialVCS(options, out.strip())
Packit bd1cd8
  except OSError, (errno, message):
Packit bd1cd8
    if errno != 2:  # ENOENT -- they don't have hg installed.
Packit bd1cd8
      raise
Packit bd1cd8
Packit bd1cd8
  # Subversion has a .svn in all working directories.
Packit bd1cd8
  if os.path.isdir('.svn'):
Packit bd1cd8
    logging.info("Guessed VCS = Subversion")
Packit bd1cd8
    return SubversionVCS(options)
Packit bd1cd8
Packit bd1cd8
  # Git has a command to test if you're in a git tree.
Packit bd1cd8
  # Try running it, but don't die if we don't have git installed.
Packit bd1cd8
  try:
Packit bd1cd8
    out, returncode = RunShellWithReturnCode(["git", "rev-parse",
Packit bd1cd8
                                              "--is-inside-work-tree"])
Packit bd1cd8
    if returncode == 0:
Packit bd1cd8
      return GitVCS(options)
Packit bd1cd8
  except OSError, (errno, message):
Packit bd1cd8
    if errno != 2:  # ENOENT -- they don't have git installed.
Packit bd1cd8
      raise
Packit bd1cd8
Packit bd1cd8
  ErrorExit(("Could not guess version control system. "
Packit bd1cd8
             "Are you in a working copy directory?"))
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def RealMain(argv, data=None):
Packit bd1cd8
  """The real main function.
Packit bd1cd8
Packit bd1cd8
  Args:
Packit bd1cd8
    argv: Command line arguments.
Packit bd1cd8
    data: Diff contents. If None (default) the diff is generated by
Packit bd1cd8
      the VersionControlSystem implementation returned by GuessVCS().
Packit bd1cd8
Packit bd1cd8
  Returns:
Packit bd1cd8
    A 2-tuple (issue id, patchset id).
Packit bd1cd8
    The patchset id is None if the base files are not uploaded by this
Packit bd1cd8
    script (applies only to SVN checkouts).
Packit bd1cd8
  """
Packit bd1cd8
  logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
Packit bd1cd8
                              "%(lineno)s %(message)s "))
Packit bd1cd8
  os.environ['LC_ALL'] = 'C'
Packit bd1cd8
  options, args = parser.parse_args(argv[1:])
Packit bd1cd8
  global verbosity
Packit bd1cd8
  verbosity = options.verbose
Packit bd1cd8
  if verbosity >= 3:
Packit bd1cd8
    logging.getLogger().setLevel(logging.DEBUG)
Packit bd1cd8
  elif verbosity >= 2:
Packit bd1cd8
    logging.getLogger().setLevel(logging.INFO)
Packit bd1cd8
  vcs = GuessVCS(options)
Packit bd1cd8
  if isinstance(vcs, SubversionVCS):
Packit bd1cd8
    # base field is only allowed for Subversion.
Packit bd1cd8
    # Note: Fetching base files may become deprecated in future releases.
Packit bd1cd8
    base = vcs.GuessBase(options.download_base)
Packit bd1cd8
  else:
Packit bd1cd8
    base = None
Packit bd1cd8
  if not base and options.download_base:
Packit bd1cd8
    options.download_base = True
Packit bd1cd8
    logging.info("Enabled upload of base file")
Packit bd1cd8
  if not options.assume_yes:
Packit bd1cd8
    vcs.CheckForUnknownFiles()
Packit bd1cd8
  if data is None:
Packit bd1cd8
    data = vcs.GenerateDiff(args)
Packit bd1cd8
  files = vcs.GetBaseFiles(data)
Packit bd1cd8
  if verbosity >= 1:
Packit bd1cd8
    print "Upload server:", options.server, "(change with -s/--server)"
Packit bd1cd8
  if options.issue:
Packit bd1cd8
    prompt = "Message describing this patch set: "
Packit bd1cd8
  else:
Packit bd1cd8
    prompt = "New issue subject: "
Packit bd1cd8
  message = options.message or raw_input(prompt).strip()
Packit bd1cd8
  if not message:
Packit bd1cd8
    ErrorExit("A non-empty message is required")
Packit bd1cd8
  rpc_server = GetRpcServer(options)
Packit bd1cd8
  form_fields = [("subject", message)]
Packit bd1cd8
  if base:
Packit bd1cd8
    form_fields.append(("base", base))
Packit bd1cd8
  if options.issue:
Packit bd1cd8
    form_fields.append(("issue", str(options.issue)))
Packit bd1cd8
  if options.email:
Packit bd1cd8
    form_fields.append(("user", options.email))
Packit bd1cd8
  if options.reviewers:
Packit bd1cd8
    for reviewer in options.reviewers.split(','):
Packit bd1cd8
      if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1:
Packit bd1cd8
        ErrorExit("Invalid email address: %s" % reviewer)
Packit bd1cd8
    form_fields.append(("reviewers", options.reviewers))
Packit bd1cd8
  if options.cc:
Packit bd1cd8
    for cc in options.cc.split(','):
Packit bd1cd8
      if "@" in cc and not cc.split("@")[1].count(".") == 1:
Packit bd1cd8
        ErrorExit("Invalid email address: %s" % cc)
Packit bd1cd8
    form_fields.append(("cc", options.cc))
Packit bd1cd8
  description = options.description
Packit bd1cd8
  if options.description_file:
Packit bd1cd8
    if options.description:
Packit bd1cd8
      ErrorExit("Can't specify description and description_file")
Packit bd1cd8
    file = open(options.description_file, 'r')
Packit bd1cd8
    description = file.read()
Packit bd1cd8
    file.close()
Packit bd1cd8
  if description:
Packit bd1cd8
    form_fields.append(("description", description))
Packit bd1cd8
  # Send a hash of all the base file so the server can determine if a copy
Packit bd1cd8
  # already exists in an earlier patchset.
Packit bd1cd8
  base_hashes = ""
Packit bd1cd8
  for file, info in files.iteritems():
Packit bd1cd8
    if not info[0] is None:
Packit bd1cd8
      checksum = md5.new(info[0]).hexdigest()
Packit bd1cd8
      if base_hashes:
Packit bd1cd8
        base_hashes += "|"
Packit bd1cd8
      base_hashes += checksum + ":" + file
Packit bd1cd8
  form_fields.append(("base_hashes", base_hashes))
Packit bd1cd8
  # If we're uploading base files, don't send the email before the uploads, so
Packit bd1cd8
  # that it contains the file status.
Packit bd1cd8
  if options.send_mail and options.download_base:
Packit bd1cd8
    form_fields.append(("send_mail", "1"))
Packit bd1cd8
  if not options.download_base:
Packit bd1cd8
    form_fields.append(("content_upload", "1"))
Packit bd1cd8
  if len(data) > MAX_UPLOAD_SIZE:
Packit bd1cd8
    print "Patch is large, so uploading file patches separately."
Packit bd1cd8
    uploaded_diff_file = []
Packit bd1cd8
    form_fields.append(("separate_patches", "1"))
Packit bd1cd8
  else:
Packit bd1cd8
    uploaded_diff_file = [("data", "data.diff", data)]
Packit bd1cd8
  ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
Packit bd1cd8
  response_body = rpc_server.Send("/upload", body, content_type=ctype)
Packit bd1cd8
  patchset = None
Packit bd1cd8
  if not options.download_base or not uploaded_diff_file:
Packit bd1cd8
    lines = response_body.splitlines()
Packit bd1cd8
    if len(lines) >= 2:
Packit bd1cd8
      msg = lines[0]
Packit bd1cd8
      patchset = lines[1].strip()
Packit bd1cd8
      patches = [x.split(" ", 1) for x in lines[2:]]
Packit bd1cd8
    else:
Packit bd1cd8
      msg = response_body
Packit bd1cd8
  else:
Packit bd1cd8
    msg = response_body
Packit bd1cd8
  StatusUpdate(msg)
Packit bd1cd8
  if not response_body.startswith("Issue created.") and \
Packit bd1cd8
  not response_body.startswith("Issue updated."):
Packit bd1cd8
    sys.exit(0)
Packit bd1cd8
  issue = msg[msg.rfind("/")+1:]
Packit bd1cd8
Packit bd1cd8
  if not uploaded_diff_file:
Packit bd1cd8
    result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
Packit bd1cd8
    if not options.download_base:
Packit bd1cd8
      patches = result
Packit bd1cd8
Packit bd1cd8
  if not options.download_base:
Packit bd1cd8
    vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
Packit bd1cd8
    if options.send_mail:
Packit bd1cd8
      rpc_server.Send("/" + issue + "/mail", payload="")
Packit bd1cd8
  return issue, patchset
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
def main():
Packit bd1cd8
  try:
Packit bd1cd8
    RealMain(sys.argv)
Packit bd1cd8
  except KeyboardInterrupt:
Packit bd1cd8
    print
Packit bd1cd8
    StatusUpdate("Interrupted.")
Packit bd1cd8
    sys.exit(1)
Packit bd1cd8
Packit bd1cd8
Packit bd1cd8
if __name__ == "__main__":
Packit bd1cd8
  main()