github.com/jlmucb/cloudproxy@v0.0.0-20170830161738-b5aa0b619bc4/src/third_party/googlemock/scripts/upload.py (about)

     1  #!/usr/bin/env python
     2  #
     3  # Copyright 2007 Google Inc.
     4  #
     5  # Licensed under the Apache License, Version 2.0 (the "License");
     6  # you may not use this file except in compliance with the License.
     7  # You may obtain a copy of the License at
     8  #
     9  #     http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  
    17  """Tool for uploading diffs from a version control system to the codereview app.
    18  
    19  Usage summary: upload.py [options] [-- diff_options]
    20  
    21  Diff options are passed to the diff command of the underlying system.
    22  
    23  Supported version control systems:
    24    Git
    25    Mercurial
    26    Subversion
    27  
    28  It is important for Git/Mercurial users to specify a tree/node/branch to diff
    29  against by using the '--rev' option.
    30  """
    31  # This code is derived from appcfg.py in the App Engine SDK (open source),
    32  # and from ASPN recipe #146306.
    33  
    34  import cookielib
    35  import getpass
    36  import logging
    37  import md5
    38  import mimetypes
    39  import optparse
    40  import os
    41  import re
    42  import socket
    43  import subprocess
    44  import sys
    45  import urllib
    46  import urllib2
    47  import urlparse
    48  
    49  try:
    50    import readline
    51  except ImportError:
    52    pass
    53  
    54  # The logging verbosity:
    55  #  0: Errors only.
    56  #  1: Status messages.
    57  #  2: Info logs.
    58  #  3: Debug logs.
    59  verbosity = 1
    60  
    61  # Max size of patch or base file.
    62  MAX_UPLOAD_SIZE = 900 * 1024
    63  
    64  
    65  def GetEmail(prompt):
    66    """Prompts the user for their email address and returns it.
    67  
    68    The last used email address is saved to a file and offered up as a suggestion
    69    to the user. If the user presses enter without typing in anything the last
    70    used email address is used. If the user enters a new address, it is saved
    71    for next time we prompt.
    72  
    73    """
    74    last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
    75    last_email = ""
    76    if os.path.exists(last_email_file_name):
    77      try:
    78        last_email_file = open(last_email_file_name, "r")
    79        last_email = last_email_file.readline().strip("\n")
    80        last_email_file.close()
    81        prompt += " [%s]" % last_email
    82      except IOError, e:
    83        pass
    84    email = raw_input(prompt + ": ").strip()
    85    if email:
    86      try:
    87        last_email_file = open(last_email_file_name, "w")
    88        last_email_file.write(email)
    89        last_email_file.close()
    90      except IOError, e:
    91        pass
    92    else:
    93      email = last_email
    94    return email
    95  
    96  
    97  def StatusUpdate(msg):
    98    """Print a status message to stdout.
    99  
   100    If 'verbosity' is greater than 0, print the message.
   101  
   102    Args:
   103      msg: The string to print.
   104    """
   105    if verbosity > 0:
   106      print msg
   107  
   108  
   109  def ErrorExit(msg):
   110    """Print an error message to stderr and exit."""
   111    print >>sys.stderr, msg
   112    sys.exit(1)
   113  
   114  
   115  class ClientLoginError(urllib2.HTTPError):
   116    """Raised to indicate there was an error authenticating with ClientLogin."""
   117  
   118    def __init__(self, url, code, msg, headers, args):
   119      urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
   120      self.args = args
   121      self.reason = args["Error"]
   122  
   123  
   124  class AbstractRpcServer(object):
   125    """Provides a common interface for a simple RPC server."""
   126  
   127    def __init__(self, host, auth_function, host_override=None, extra_headers={},
   128                 save_cookies=False):
   129      """Creates a new HttpRpcServer.
   130  
   131      Args:
   132        host: The host to send requests to.
   133        auth_function: A function that takes no arguments and returns an
   134          (email, password) tuple when called. Will be called if authentication
   135          is required.
   136        host_override: The host header to send to the server (defaults to host).
   137        extra_headers: A dict of extra headers to append to every request.
   138        save_cookies: If True, save the authentication cookies to local disk.
   139          If False, use an in-memory cookiejar instead.  Subclasses must
   140          implement this functionality.  Defaults to False.
   141      """
   142      self.host = host
   143      self.host_override = host_override
   144      self.auth_function = auth_function
   145      self.authenticated = False
   146      self.extra_headers = extra_headers
   147      self.save_cookies = save_cookies
   148      self.opener = self._GetOpener()
   149      if self.host_override:
   150        logging.info("Server: %s; Host: %s", self.host, self.host_override)
   151      else:
   152        logging.info("Server: %s", self.host)
   153  
   154    def _GetOpener(self):
   155      """Returns an OpenerDirector for making HTTP requests.
   156  
   157      Returns:
   158        A urllib2.OpenerDirector object.
   159      """
   160      raise NotImplementedError()
   161  
   162    def _CreateRequest(self, url, data=None):
   163      """Creates a new urllib request."""
   164      logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
   165      req = urllib2.Request(url, data=data)
   166      if self.host_override:
   167        req.add_header("Host", self.host_override)
   168      for key, value in self.extra_headers.iteritems():
   169        req.add_header(key, value)
   170      return req
   171  
   172    def _GetAuthToken(self, email, password):
   173      """Uses ClientLogin to authenticate the user, returning an auth token.
   174  
   175      Args:
   176        email:    The user's email address
   177        password: The user's password
   178  
   179      Raises:
   180        ClientLoginError: If there was an error authenticating with ClientLogin.
   181        HTTPError: If there was some other form of HTTP error.
   182  
   183      Returns:
   184        The authentication token returned by ClientLogin.
   185      """
   186      account_type = "GOOGLE"
   187      if self.host.endswith(".google.com"):
   188        # Needed for use inside Google.
   189        account_type = "HOSTED"
   190      req = self._CreateRequest(
   191          url="https://www.google.com/accounts/ClientLogin",
   192          data=urllib.urlencode({
   193              "Email": email,
   194              "Passwd": password,
   195              "service": "ah",
   196              "source": "rietveld-codereview-upload",
   197              "accountType": account_type,
   198          }),
   199      )
   200      try:
   201        response = self.opener.open(req)
   202        response_body = response.read()
   203        response_dict = dict(x.split("=")
   204                             for x in response_body.split("\n") if x)
   205        return response_dict["Auth"]
   206      except urllib2.HTTPError, e:
   207        if e.code == 403:
   208          body = e.read()
   209          response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
   210          raise ClientLoginError(req.get_full_url(), e.code, e.msg,
   211                                 e.headers, response_dict)
   212        else:
   213          raise
   214  
   215    def _GetAuthCookie(self, auth_token):
   216      """Fetches authentication cookies for an authentication token.
   217  
   218      Args:
   219        auth_token: The authentication token returned by ClientLogin.
   220  
   221      Raises:
   222        HTTPError: If there was an error fetching the authentication cookies.
   223      """
   224      # This is a dummy value to allow us to identify when we're successful.
   225      continue_location = "http://localhost/"
   226      args = {"continue": continue_location, "auth": auth_token}
   227      req = self._CreateRequest("http://%s/_ah/login?%s" %
   228                                (self.host, urllib.urlencode(args)))
   229      try:
   230        response = self.opener.open(req)
   231      except urllib2.HTTPError, e:
   232        response = e
   233      if (response.code != 302 or
   234          response.info()["location"] != continue_location):
   235        raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
   236                                response.headers, response.fp)
   237      self.authenticated = True
   238  
   239    def _Authenticate(self):
   240      """Authenticates the user.
   241  
   242      The authentication process works as follows:
   243       1) We get a username and password from the user
   244       2) We use ClientLogin to obtain an AUTH token for the user
   245          (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
   246       3) We pass the auth token to /_ah/login on the server to obtain an
   247          authentication cookie. If login was successful, it tries to redirect
   248          us to the URL we provided.
   249  
   250      If we attempt to access the upload API without first obtaining an
   251      authentication cookie, it returns a 401 response and directs us to
   252      authenticate ourselves with ClientLogin.
   253      """
   254      for i in range(3):
   255        credentials = self.auth_function()
   256        try:
   257          auth_token = self._GetAuthToken(credentials[0], credentials[1])
   258        except ClientLoginError, e:
   259          if e.reason == "BadAuthentication":
   260            print >>sys.stderr, "Invalid username or password."
   261            continue
   262          if e.reason == "CaptchaRequired":
   263            print >>sys.stderr, (
   264                "Please go to\n"
   265                "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
   266                "and verify you are a human.  Then try again.")
   267            break
   268          if e.reason == "NotVerified":
   269            print >>sys.stderr, "Account not verified."
   270            break
   271          if e.reason == "TermsNotAgreed":
   272            print >>sys.stderr, "User has not agreed to TOS."
   273            break
   274          if e.reason == "AccountDeleted":
   275            print >>sys.stderr, "The user account has been deleted."
   276            break
   277          if e.reason == "AccountDisabled":
   278            print >>sys.stderr, "The user account has been disabled."
   279            break
   280          if e.reason == "ServiceDisabled":
   281            print >>sys.stderr, ("The user's access to the service has been "
   282                                 "disabled.")
   283            break
   284          if e.reason == "ServiceUnavailable":
   285            print >>sys.stderr, "The service is not available; try again later."
   286            break
   287          raise
   288        self._GetAuthCookie(auth_token)
   289        return
   290  
   291    def Send(self, request_path, payload=None,
   292             content_type="application/octet-stream",
   293             timeout=None,
   294             **kwargs):
   295      """Sends an RPC and returns the response.
   296  
   297      Args:
   298        request_path: The path to send the request to, eg /api/appversion/create.
   299        payload: The body of the request, or None to send an empty request.
   300        content_type: The Content-Type header to use.
   301        timeout: timeout in seconds; default None i.e. no timeout.
   302          (Note: for large requests on OS X, the timeout doesn't work right.)
   303        kwargs: Any keyword arguments are converted into query string parameters.
   304  
   305      Returns:
   306        The response body, as a string.
   307      """
   308      # TODO: Don't require authentication.  Let the server say
   309      # whether it is necessary.
   310      if not self.authenticated:
   311        self._Authenticate()
   312  
   313      old_timeout = socket.getdefaulttimeout()
   314      socket.setdefaulttimeout(timeout)
   315      try:
   316        tries = 0
   317        while True:
   318          tries += 1
   319          args = dict(kwargs)
   320          url = "http://%s%s" % (self.host, request_path)
   321          if args:
   322            url += "?" + urllib.urlencode(args)
   323          req = self._CreateRequest(url=url, data=payload)
   324          req.add_header("Content-Type", content_type)
   325          try:
   326            f = self.opener.open(req)
   327            response = f.read()
   328            f.close()
   329            return response
   330          except urllib2.HTTPError, e:
   331            if tries > 3:
   332              raise
   333            elif e.code == 401:
   334              self._Authenticate()
   335  ##           elif e.code >= 500 and e.code < 600:
   336  ##             # Server Error - try again.
   337  ##             continue
   338            else:
   339              raise
   340      finally:
   341        socket.setdefaulttimeout(old_timeout)
   342  
   343  
   344  class HttpRpcServer(AbstractRpcServer):
   345    """Provides a simplified RPC-style interface for HTTP requests."""
   346  
   347    def _Authenticate(self):
   348      """Save the cookie jar after authentication."""
   349      super(HttpRpcServer, self)._Authenticate()
   350      if self.save_cookies:
   351        StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
   352        self.cookie_jar.save()
   353  
   354    def _GetOpener(self):
   355      """Returns an OpenerDirector that supports cookies and ignores redirects.
   356  
   357      Returns:
   358        A urllib2.OpenerDirector object.
   359      """
   360      opener = urllib2.OpenerDirector()
   361      opener.add_handler(urllib2.ProxyHandler())
   362      opener.add_handler(urllib2.UnknownHandler())
   363      opener.add_handler(urllib2.HTTPHandler())
   364      opener.add_handler(urllib2.HTTPDefaultErrorHandler())
   365      opener.add_handler(urllib2.HTTPSHandler())
   366      opener.add_handler(urllib2.HTTPErrorProcessor())
   367      if self.save_cookies:
   368        self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
   369        self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
   370        if os.path.exists(self.cookie_file):
   371          try:
   372            self.cookie_jar.load()
   373            self.authenticated = True
   374            StatusUpdate("Loaded authentication cookies from %s" %
   375                         self.cookie_file)
   376          except (cookielib.LoadError, IOError):
   377            # Failed to load cookies - just ignore them.
   378            pass
   379        else:
   380          # Create an empty cookie file with mode 600
   381          fd = os.open(self.cookie_file, os.O_CREAT, 0600)
   382          os.close(fd)
   383        # Always chmod the cookie file
   384        os.chmod(self.cookie_file, 0600)
   385      else:
   386        # Don't save cookies across runs of update.py.
   387        self.cookie_jar = cookielib.CookieJar()
   388      opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
   389      return opener
   390  
   391  
   392  parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
   393  parser.add_option("-y", "--assume_yes", action="store_true",
   394                    dest="assume_yes", default=False,
   395                    help="Assume that the answer to yes/no questions is 'yes'.")
   396  # Logging
   397  group = parser.add_option_group("Logging options")
   398  group.add_option("-q", "--quiet", action="store_const", const=0,
   399                   dest="verbose", help="Print errors only.")
   400  group.add_option("-v", "--verbose", action="store_const", const=2,
   401                   dest="verbose", default=1,
   402                   help="Print info level logs (default).")
   403  group.add_option("--noisy", action="store_const", const=3,
   404                   dest="verbose", help="Print all logs.")
   405  # Review server
   406  group = parser.add_option_group("Review server options")
   407  group.add_option("-s", "--server", action="store", dest="server",
   408                   default="codereview.appspot.com",
   409                   metavar="SERVER",
   410                   help=("The server to upload to. The format is host[:port]. "
   411                         "Defaults to 'codereview.appspot.com'."))
   412  group.add_option("-e", "--email", action="store", dest="email",
   413                   metavar="EMAIL", default=None,
   414                   help="The username to use. Will prompt if omitted.")
   415  group.add_option("-H", "--host", action="store", dest="host",
   416                   metavar="HOST", default=None,
   417                   help="Overrides the Host header sent with all RPCs.")
   418  group.add_option("--no_cookies", action="store_false",
   419                   dest="save_cookies", default=True,
   420                   help="Do not save authentication cookies to local disk.")
   421  # Issue
   422  group = parser.add_option_group("Issue options")
   423  group.add_option("-d", "--description", action="store", dest="description",
   424                   metavar="DESCRIPTION", default=None,
   425                   help="Optional description when creating an issue.")
   426  group.add_option("-f", "--description_file", action="store",
   427                   dest="description_file", metavar="DESCRIPTION_FILE",
   428                   default=None,
   429                   help="Optional path of a file that contains "
   430                        "the description when creating an issue.")
   431  group.add_option("-r", "--reviewers", action="store", dest="reviewers",
   432                   metavar="REVIEWERS", default=None,
   433                   help="Add reviewers (comma separated email addresses).")
   434  group.add_option("--cc", action="store", dest="cc",
   435                   metavar="CC", default=None,
   436                   help="Add CC (comma separated email addresses).")
   437  # Upload options
   438  group = parser.add_option_group("Patch options")
   439  group.add_option("-m", "--message", action="store", dest="message",
   440                   metavar="MESSAGE", default=None,
   441                   help="A message to identify the patch. "
   442                        "Will prompt if omitted.")
   443  group.add_option("-i", "--issue", type="int", action="store",
   444                   metavar="ISSUE", default=None,
   445                   help="Issue number to which to add. Defaults to new issue.")
   446  group.add_option("--download_base", action="store_true",
   447                   dest="download_base", default=False,
   448                   help="Base files will be downloaded by the server "
   449                   "(side-by-side diffs may not work on files with CRs).")
   450  group.add_option("--rev", action="store", dest="revision",
   451                   metavar="REV", default=None,
   452                   help="Branch/tree/revision to diff against (used by DVCS).")
   453  group.add_option("--send_mail", action="store_true",
   454                   dest="send_mail", default=False,
   455                   help="Send notification email to reviewers.")
   456  
   457  
   458  def GetRpcServer(options):
   459    """Returns an instance of an AbstractRpcServer.
   460  
   461    Returns:
   462      A new AbstractRpcServer, on which RPC calls can be made.
   463    """
   464  
   465    rpc_server_class = HttpRpcServer
   466  
   467    def GetUserCredentials():
   468      """Prompts the user for a username and password."""
   469      email = options.email
   470      if email is None:
   471        email = GetEmail("Email (login for uploading to %s)" % options.server)
   472      password = getpass.getpass("Password for %s: " % email)
   473      return (email, password)
   474  
   475    # If this is the dev_appserver, use fake authentication.
   476    host = (options.host or options.server).lower()
   477    if host == "localhost" or host.startswith("localhost:"):
   478      email = options.email
   479      if email is None:
   480        email = "test@example.com"
   481        logging.info("Using debug user %s.  Override with --email" % email)
   482      server = rpc_server_class(
   483          options.server,
   484          lambda: (email, "password"),
   485          host_override=options.host,
   486          extra_headers={"Cookie":
   487                         'dev_appserver_login="%s:False"' % email},
   488          save_cookies=options.save_cookies)
   489      # Don't try to talk to ClientLogin.
   490      server.authenticated = True
   491      return server
   492  
   493    return rpc_server_class(options.server, GetUserCredentials,
   494                            host_override=options.host,
   495                            save_cookies=options.save_cookies)
   496  
   497  
   498  def EncodeMultipartFormData(fields, files):
   499    """Encode form fields for multipart/form-data.
   500  
   501    Args:
   502      fields: A sequence of (name, value) elements for regular form fields.
   503      files: A sequence of (name, filename, value) elements for data to be
   504             uploaded as files.
   505    Returns:
   506      (content_type, body) ready for httplib.HTTP instance.
   507  
   508    Source:
   509      http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
   510    """
   511    BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
   512    CRLF = '\r\n'
   513    lines = []
   514    for (key, value) in fields:
   515      lines.append('--' + BOUNDARY)
   516      lines.append('Content-Disposition: form-data; name="%s"' % key)
   517      lines.append('')
   518      lines.append(value)
   519    for (key, filename, value) in files:
   520      lines.append('--' + BOUNDARY)
   521      lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
   522               (key, filename))
   523      lines.append('Content-Type: %s' % GetContentType(filename))
   524      lines.append('')
   525      lines.append(value)
   526    lines.append('--' + BOUNDARY + '--')
   527    lines.append('')
   528    body = CRLF.join(lines)
   529    content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
   530    return content_type, body
   531  
   532  
   533  def GetContentType(filename):
   534    """Helper to guess the content-type from the filename."""
   535    return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
   536  
   537  
   538  # Use a shell for subcommands on Windows to get a PATH search.
   539  use_shell = sys.platform.startswith("win")
   540  
   541  def RunShellWithReturnCode(command, print_output=False,
   542                             universal_newlines=True):
   543    """Executes a command and returns the output from stdout and the return code.
   544  
   545    Args:
   546      command: Command to execute.
   547      print_output: If True, the output is printed to stdout.
   548                    If False, both stdout and stderr are ignored.
   549      universal_newlines: Use universal_newlines flag (default: True).
   550  
   551    Returns:
   552      Tuple (output, return code)
   553    """
   554    logging.info("Running %s", command)
   555    p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
   556                         shell=use_shell, universal_newlines=universal_newlines)
   557    if print_output:
   558      output_array = []
   559      while True:
   560        line = p.stdout.readline()
   561        if not line:
   562          break
   563        print line.strip("\n")
   564        output_array.append(line)
   565      output = "".join(output_array)
   566    else:
   567      output = p.stdout.read()
   568    p.wait()
   569    errout = p.stderr.read()
   570    if print_output and errout:
   571      print >>sys.stderr, errout
   572    p.stdout.close()
   573    p.stderr.close()
   574    return output, p.returncode
   575  
   576  
   577  def RunShell(command, silent_ok=False, universal_newlines=True,
   578               print_output=False):
   579    data, retcode = RunShellWithReturnCode(command, print_output,
   580                                           universal_newlines)
   581    if retcode:
   582      ErrorExit("Got error status from %s:\n%s" % (command, data))
   583    if not silent_ok and not data:
   584      ErrorExit("No output from %s" % command)
   585    return data
   586  
   587  
   588  class VersionControlSystem(object):
   589    """Abstract base class providing an interface to the VCS."""
   590  
   591    def __init__(self, options):
   592      """Constructor.
   593  
   594      Args:
   595        options: Command line options.
   596      """
   597      self.options = options
   598  
   599    def GenerateDiff(self, args):
   600      """Return the current diff as a string.
   601  
   602      Args:
   603        args: Extra arguments to pass to the diff command.
   604      """
   605      raise NotImplementedError(
   606          "abstract method -- subclass %s must override" % self.__class__)
   607  
   608    def GetUnknownFiles(self):
   609      """Return a list of files unknown to the VCS."""
   610      raise NotImplementedError(
   611          "abstract method -- subclass %s must override" % self.__class__)
   612  
   613    def CheckForUnknownFiles(self):
   614      """Show an "are you sure?" prompt if there are unknown files."""
   615      unknown_files = self.GetUnknownFiles()
   616      if unknown_files:
   617        print "The following files are not added to version control:"
   618        for line in unknown_files:
   619          print line
   620        prompt = "Are you sure to continue?(y/N) "
   621        answer = raw_input(prompt).strip()
   622        if answer != "y":
   623          ErrorExit("User aborted")
   624  
   625    def GetBaseFile(self, filename):
   626      """Get the content of the upstream version of a file.
   627  
   628      Returns:
   629        A tuple (base_content, new_content, is_binary, status)
   630          base_content: The contents of the base file.
   631          new_content: For text files, this is empty.  For binary files, this is
   632            the contents of the new file, since the diff output won't contain
   633            information to reconstruct the current file.
   634          is_binary: True iff the file is binary.
   635          status: The status of the file.
   636      """
   637  
   638      raise NotImplementedError(
   639          "abstract method -- subclass %s must override" % self.__class__)
   640  
   641  
   642    def GetBaseFiles(self, diff):
   643      """Helper that calls GetBase file for each file in the patch.
   644  
   645      Returns:
   646        A dictionary that maps from filename to GetBaseFile's tuple.  Filenames
   647        are retrieved based on lines that start with "Index:" or
   648        "Property changes on:".
   649      """
   650      files = {}
   651      for line in diff.splitlines(True):
   652        if line.startswith('Index:') or line.startswith('Property changes on:'):
   653          unused, filename = line.split(':', 1)
   654          # On Windows if a file has property changes its filename uses '\'
   655          # instead of '/'.
   656          filename = filename.strip().replace('\\', '/')
   657          files[filename] = self.GetBaseFile(filename)
   658      return files
   659  
   660  
   661    def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
   662                        files):
   663      """Uploads the base files (and if necessary, the current ones as well)."""
   664  
   665      def UploadFile(filename, file_id, content, is_binary, status, is_base):
   666        """Uploads a file to the server."""
   667        file_too_large = False
   668        if is_base:
   669          type = "base"
   670        else:
   671          type = "current"
   672        if len(content) > MAX_UPLOAD_SIZE:
   673          print ("Not uploading the %s file for %s because it's too large." %
   674                 (type, filename))
   675          file_too_large = True
   676          content = ""
   677        checksum = md5.new(content).hexdigest()
   678        if options.verbose > 0 and not file_too_large:
   679          print "Uploading %s file for %s" % (type, filename)
   680        url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
   681        form_fields = [("filename", filename),
   682                       ("status", status),
   683                       ("checksum", checksum),
   684                       ("is_binary", str(is_binary)),
   685                       ("is_current", str(not is_base)),
   686                      ]
   687        if file_too_large:
   688          form_fields.append(("file_too_large", "1"))
   689        if options.email:
   690          form_fields.append(("user", options.email))
   691        ctype, body = EncodeMultipartFormData(form_fields,
   692                                              [("data", filename, content)])
   693        response_body = rpc_server.Send(url, body,
   694                                        content_type=ctype)
   695        if not response_body.startswith("OK"):
   696          StatusUpdate("  --> %s" % response_body)
   697          sys.exit(1)
   698  
   699      patches = dict()
   700      [patches.setdefault(v, k) for k, v in patch_list]
   701      for filename in patches.keys():
   702        base_content, new_content, is_binary, status = files[filename]
   703        file_id_str = patches.get(filename)
   704        if file_id_str.find("nobase") != -1:
   705          base_content = None
   706          file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
   707        file_id = int(file_id_str)
   708        if base_content != None:
   709          UploadFile(filename, file_id, base_content, is_binary, status, True)
   710        if new_content != None:
   711          UploadFile(filename, file_id, new_content, is_binary, status, False)
   712  
   713    def IsImage(self, filename):
   714      """Returns true if the filename has an image extension."""
   715      mimetype =  mimetypes.guess_type(filename)[0]
   716      if not mimetype:
   717        return False
   718      return mimetype.startswith("image/")
   719  
   720  
   721  class SubversionVCS(VersionControlSystem):
   722    """Implementation of the VersionControlSystem interface for Subversion."""
   723  
   724    def __init__(self, options):
   725      super(SubversionVCS, self).__init__(options)
   726      if self.options.revision:
   727        match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
   728        if not match:
   729          ErrorExit("Invalid Subversion revision %s." % self.options.revision)
   730        self.rev_start = match.group(1)
   731        self.rev_end = match.group(3)
   732      else:
   733        self.rev_start = self.rev_end = None
   734      # Cache output from "svn list -r REVNO dirname".
   735      # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
   736      self.svnls_cache = {}
   737      # SVN base URL is required to fetch files deleted in an older revision.
   738      # Result is cached to not guess it over and over again in GetBaseFile().
   739      required = self.options.download_base or self.options.revision is not None
   740      self.svn_base = self._GuessBase(required)
   741  
   742    def GuessBase(self, required):
   743      """Wrapper for _GuessBase."""
   744      return self.svn_base
   745  
   746    def _GuessBase(self, required):
   747      """Returns the SVN base URL.
   748  
   749      Args:
   750        required: If true, exits if the url can't be guessed, otherwise None is
   751          returned.
   752      """
   753      info = RunShell(["svn", "info"])
   754      for line in info.splitlines():
   755        words = line.split()
   756        if len(words) == 2 and words[0] == "URL:":
   757          url = words[1]
   758          scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
   759          username, netloc = urllib.splituser(netloc)
   760          if username:
   761            logging.info("Removed username from base URL")
   762          if netloc.endswith("svn.python.org"):
   763            if netloc == "svn.python.org":
   764              if path.startswith("/projects/"):
   765                path = path[9:]
   766            elif netloc != "pythondev@svn.python.org":
   767              ErrorExit("Unrecognized Python URL: %s" % url)
   768            base = "http://svn.python.org/view/*checkout*%s/" % path
   769            logging.info("Guessed Python base = %s", base)
   770          elif netloc.endswith("svn.collab.net"):
   771            if path.startswith("/repos/"):
   772              path = path[6:]
   773            base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
   774            logging.info("Guessed CollabNet base = %s", base)
   775          elif netloc.endswith(".googlecode.com"):
   776            path = path + "/"
   777            base = urlparse.urlunparse(("http", netloc, path, params,
   778                                        query, fragment))
   779            logging.info("Guessed Google Code base = %s", base)
   780          else:
   781            path = path + "/"
   782            base = urlparse.urlunparse((scheme, netloc, path, params,
   783                                        query, fragment))
   784            logging.info("Guessed base = %s", base)
   785          return base
   786      if required:
   787        ErrorExit("Can't find URL in output from svn info")
   788      return None
   789  
   790    def GenerateDiff(self, args):
   791      cmd = ["svn", "diff"]
   792      if self.options.revision:
   793        cmd += ["-r", self.options.revision]
   794      cmd.extend(args)
   795      data = RunShell(cmd)
   796      count = 0
   797      for line in data.splitlines():
   798        if line.startswith("Index:") or line.startswith("Property changes on:"):
   799          count += 1
   800          logging.info(line)
   801      if not count:
   802        ErrorExit("No valid patches found in output from svn diff")
   803      return data
   804  
   805    def _CollapseKeywords(self, content, keyword_str):
   806      """Collapses SVN keywords."""
   807      # svn cat translates keywords but svn diff doesn't. As a result of this
   808      # behavior patching.PatchChunks() fails with a chunk mismatch error.
   809      # This part was originally written by the Review Board development team
   810      # who had the same problem (http://reviews.review-board.org/r/276/).
   811      # Mapping of keywords to known aliases
   812      svn_keywords = {
   813        # Standard keywords
   814        'Date':                ['Date', 'LastChangedDate'],
   815        'Revision':            ['Revision', 'LastChangedRevision', 'Rev'],
   816        'Author':              ['Author', 'LastChangedBy'],
   817        'HeadURL':             ['HeadURL', 'URL'],
   818        'Id':                  ['Id'],
   819  
   820        # Aliases
   821        'LastChangedDate':     ['LastChangedDate', 'Date'],
   822        'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
   823        'LastChangedBy':       ['LastChangedBy', 'Author'],
   824        'URL':                 ['URL', 'HeadURL'],
   825      }
   826  
   827      def repl(m):
   828         if m.group(2):
   829           return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
   830         return "$%s$" % m.group(1)
   831      keywords = [keyword
   832                  for name in keyword_str.split(" ")
   833                  for keyword in svn_keywords.get(name, [])]
   834      return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
   835  
   836    def GetUnknownFiles(self):
   837      status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
   838      unknown_files = []
   839      for line in status.split("\n"):
   840        if line and line[0] == "?":
   841          unknown_files.append(line)
   842      return unknown_files
   843  
   844    def ReadFile(self, filename):
   845      """Returns the contents of a file."""
   846      file = open(filename, 'rb')
   847      result = ""
   848      try:
   849        result = file.read()
   850      finally:
   851        file.close()
   852      return result
   853  
   854    def GetStatus(self, filename):
   855      """Returns the status of a file."""
   856      if not self.options.revision:
   857        status = RunShell(["svn", "status", "--ignore-externals", filename])
   858        if not status:
   859          ErrorExit("svn status returned no output for %s" % filename)
   860        status_lines = status.splitlines()
   861        # If file is in a cl, the output will begin with
   862        # "\n--- Changelist 'cl_name':\n".  See
   863        # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
   864        if (len(status_lines) == 3 and
   865            not status_lines[0] and
   866            status_lines[1].startswith("--- Changelist")):
   867          status = status_lines[2]
   868        else:
   869          status = status_lines[0]
   870      # If we have a revision to diff against we need to run "svn list"
   871      # for the old and the new revision and compare the results to get
   872      # the correct status for a file.
   873      else:
   874        dirname, relfilename = os.path.split(filename)
   875        if dirname not in self.svnls_cache:
   876          cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
   877          out, returncode = RunShellWithReturnCode(cmd)
   878          if returncode:
   879            ErrorExit("Failed to get status for %s." % filename)
   880          old_files = out.splitlines()
   881          args = ["svn", "list"]
   882          if self.rev_end:
   883            args += ["-r", self.rev_end]
   884          cmd = args + [dirname or "."]
   885          out, returncode = RunShellWithReturnCode(cmd)
   886          if returncode:
   887            ErrorExit("Failed to run command %s" % cmd)
   888          self.svnls_cache[dirname] = (old_files, out.splitlines())
   889        old_files, new_files = self.svnls_cache[dirname]
   890        if relfilename in old_files and relfilename not in new_files:
   891          status = "D   "
   892        elif relfilename in old_files and relfilename in new_files:
   893          status = "M   "
   894        else:
   895          status = "A   "
   896      return status
   897  
   898    def GetBaseFile(self, filename):
   899      status = self.GetStatus(filename)
   900      base_content = None
   901      new_content = None
   902  
   903      # If a file is copied its status will be "A  +", which signifies
   904      # "addition-with-history".  See "svn st" for more information.  We need to
   905      # upload the original file or else diff parsing will fail if the file was
   906      # edited.
   907      if status[0] == "A" and status[3] != "+":
   908        # We'll need to upload the new content if we're adding a binary file
   909        # since diff's output won't contain it.
   910        mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
   911                            silent_ok=True)
   912        base_content = ""
   913        is_binary = mimetype and not mimetype.startswith("text/")
   914        if is_binary and self.IsImage(filename):
   915          new_content = self.ReadFile(filename)
   916      elif (status[0] in ("M", "D", "R") or
   917            (status[0] == "A" and status[3] == "+") or  # Copied file.
   918            (status[0] == " " and status[1] == "M")):  # Property change.
   919        args = []
   920        if self.options.revision:
   921          url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
   922        else:
   923          # Don't change filename, it's needed later.
   924          url = filename
   925          args += ["-r", "BASE"]
   926        cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
   927        mimetype, returncode = RunShellWithReturnCode(cmd)
   928        if returncode:
   929          # File does not exist in the requested revision.
   930          # Reset mimetype, it contains an error message.
   931          mimetype = ""
   932        get_base = False
   933        is_binary = mimetype and not mimetype.startswith("text/")
   934        if status[0] == " ":
   935          # Empty base content just to force an upload.
   936          base_content = ""
   937        elif is_binary:
   938          if self.IsImage(filename):
   939            get_base = True
   940            if status[0] == "M":
   941              if not self.rev_end:
   942                new_content = self.ReadFile(filename)
   943              else:
   944                url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
   945                new_content = RunShell(["svn", "cat", url],
   946                                       universal_newlines=True, silent_ok=True)
   947          else:
   948            base_content = ""
   949        else:
   950          get_base = True
   951  
   952        if get_base:
   953          if is_binary:
   954            universal_newlines = False
   955          else:
   956            universal_newlines = True
   957          if self.rev_start:
   958            # "svn cat -r REV delete_file.txt" doesn't work. cat requires
   959            # the full URL with "@REV" appended instead of using "-r" option.
   960            url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
   961            base_content = RunShell(["svn", "cat", url],
   962                                    universal_newlines=universal_newlines,
   963                                    silent_ok=True)
   964          else:
   965            base_content = RunShell(["svn", "cat", filename],
   966                                    universal_newlines=universal_newlines,
   967                                    silent_ok=True)
   968          if not is_binary:
   969            args = []
   970            if self.rev_start:
   971              url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
   972            else:
   973              url = filename
   974              args += ["-r", "BASE"]
   975            cmd = ["svn"] + args + ["propget", "svn:keywords", url]
   976            keywords, returncode = RunShellWithReturnCode(cmd)
   977            if keywords and not returncode:
   978              base_content = self._CollapseKeywords(base_content, keywords)
   979      else:
   980        StatusUpdate("svn status returned unexpected output: %s" % status)
   981        sys.exit(1)
   982      return base_content, new_content, is_binary, status[0:5]
   983  
   984  
   985  class GitVCS(VersionControlSystem):
   986    """Implementation of the VersionControlSystem interface for Git."""
   987  
   988    def __init__(self, options):
   989      super(GitVCS, self).__init__(options)
   990      # Map of filename -> hash of base file.
   991      self.base_hashes = {}
   992  
   993    def GenerateDiff(self, extra_args):
   994      # This is more complicated than svn's GenerateDiff because we must convert
   995      # the diff output to include an svn-style "Index:" line as well as record
   996      # the hashes of the base files, so we can upload them along with our diff.
   997      if self.options.revision:
   998        extra_args = [self.options.revision] + extra_args
   999      gitdiff = RunShell(["git", "diff", "--full-index"] + extra_args)
  1000      svndiff = []
  1001      filecount = 0
  1002      filename = None
  1003      for line in gitdiff.splitlines():
  1004        match = re.match(r"diff --git a/(.*) b/.*$", line)
  1005        if match:
  1006          filecount += 1
  1007          filename = match.group(1)
  1008          svndiff.append("Index: %s\n" % filename)
  1009        else:
  1010          # The "index" line in a git diff looks like this (long hashes elided):
  1011          #   index 82c0d44..b2cee3f 100755
  1012          # We want to save the left hash, as that identifies the base file.
  1013          match = re.match(r"index (\w+)\.\.", line)
  1014          if match:
  1015            self.base_hashes[filename] = match.group(1)
  1016        svndiff.append(line + "\n")
  1017      if not filecount:
  1018        ErrorExit("No valid patches found in output from git diff")
  1019      return "".join(svndiff)
  1020  
  1021    def GetUnknownFiles(self):
  1022      status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
  1023                        silent_ok=True)
  1024      return status.splitlines()
  1025  
  1026    def GetBaseFile(self, filename):
  1027      hash = self.base_hashes[filename]
  1028      base_content = None
  1029      new_content = None
  1030      is_binary = False
  1031      if hash == "0" * 40:  # All-zero hash indicates no base file.
  1032        status = "A"
  1033        base_content = ""
  1034      else:
  1035        status = "M"
  1036        base_content, returncode = RunShellWithReturnCode(["git", "show", hash])
  1037        if returncode:
  1038          ErrorExit("Got error status from 'git show %s'" % hash)
  1039      return (base_content, new_content, is_binary, status)
  1040  
  1041  
  1042  class MercurialVCS(VersionControlSystem):
  1043    """Implementation of the VersionControlSystem interface for Mercurial."""
  1044  
  1045    def __init__(self, options, repo_dir):
  1046      super(MercurialVCS, self).__init__(options)
  1047      # Absolute path to repository (we can be in a subdir)
  1048      self.repo_dir = os.path.normpath(repo_dir)
  1049      # Compute the subdir
  1050      cwd = os.path.normpath(os.getcwd())
  1051      assert cwd.startswith(self.repo_dir)
  1052      self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
  1053      if self.options.revision:
  1054        self.base_rev = self.options.revision
  1055      else:
  1056        self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
  1057  
  1058    def _GetRelPath(self, filename):
  1059      """Get relative path of a file according to the current directory,
  1060      given its logical path in the repo."""
  1061      assert filename.startswith(self.subdir), filename
  1062      return filename[len(self.subdir):].lstrip(r"\/")
  1063  
  1064    def GenerateDiff(self, extra_args):
  1065      # If no file specified, restrict to the current subdir
  1066      extra_args = extra_args or ["."]
  1067      cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
  1068      data = RunShell(cmd, silent_ok=True)
  1069      svndiff = []
  1070      filecount = 0
  1071      for line in data.splitlines():
  1072        m = re.match("diff --git a/(\S+) b/(\S+)", line)
  1073        if m:
  1074          # Modify line to make it look like as it comes from svn diff.
  1075          # With this modification no changes on the server side are required
  1076          # to make upload.py work with Mercurial repos.
  1077          # NOTE: for proper handling of moved/copied files, we have to use
  1078          # the second filename.
  1079          filename = m.group(2)
  1080          svndiff.append("Index: %s" % filename)
  1081          svndiff.append("=" * 67)
  1082          filecount += 1
  1083          logging.info(line)
  1084        else:
  1085          svndiff.append(line)
  1086      if not filecount:
  1087        ErrorExit("No valid patches found in output from hg diff")
  1088      return "\n".join(svndiff) + "\n"
  1089  
  1090    def GetUnknownFiles(self):
  1091      """Return a list of files unknown to the VCS."""
  1092      args = []
  1093      status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
  1094          silent_ok=True)
  1095      unknown_files = []
  1096      for line in status.splitlines():
  1097        st, fn = line.split(" ", 1)
  1098        if st == "?":
  1099          unknown_files.append(fn)
  1100      return unknown_files
  1101  
  1102    def GetBaseFile(self, filename):
  1103      # "hg status" and "hg cat" both take a path relative to the current subdir
  1104      # rather than to the repo root, but "hg diff" has given us the full path
  1105      # to the repo root.
  1106      base_content = ""
  1107      new_content = None
  1108      is_binary = False
  1109      oldrelpath = relpath = self._GetRelPath(filename)
  1110      # "hg status -C" returns two lines for moved/copied files, one otherwise
  1111      out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
  1112      out = out.splitlines()
  1113      # HACK: strip error message about missing file/directory if it isn't in
  1114      # the working copy
  1115      if out[0].startswith('%s: ' % relpath):
  1116        out = out[1:]
  1117      if len(out) > 1:
  1118        # Moved/copied => considered as modified, use old filename to
  1119        # retrieve base contents
  1120        oldrelpath = out[1].strip()
  1121        status = "M"
  1122      else:
  1123        status, _ = out[0].split(' ', 1)
  1124      if status != "A":
  1125        base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
  1126          silent_ok=True)
  1127        is_binary = "\0" in base_content  # Mercurial's heuristic
  1128      if status != "R":
  1129        new_content = open(relpath, "rb").read()
  1130        is_binary = is_binary or "\0" in new_content
  1131      if is_binary and base_content:
  1132        # Fetch again without converting newlines
  1133        base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
  1134          silent_ok=True, universal_newlines=False)
  1135      if not is_binary or not self.IsImage(relpath):
  1136        new_content = None
  1137      return base_content, new_content, is_binary, status
  1138  
  1139  
  1140  # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
  1141  def SplitPatch(data):
  1142    """Splits a patch into separate pieces for each file.
  1143  
  1144    Args:
  1145      data: A string containing the output of svn diff.
  1146  
  1147    Returns:
  1148      A list of 2-tuple (filename, text) where text is the svn diff output
  1149        pertaining to filename.
  1150    """
  1151    patches = []
  1152    filename = None
  1153    diff = []
  1154    for line in data.splitlines(True):
  1155      new_filename = None
  1156      if line.startswith('Index:'):
  1157        unused, new_filename = line.split(':', 1)
  1158        new_filename = new_filename.strip()
  1159      elif line.startswith('Property changes on:'):
  1160        unused, temp_filename = line.split(':', 1)
  1161        # When a file is modified, paths use '/' between directories, however
  1162        # when a property is modified '\' is used on Windows.  Make them the same
  1163        # otherwise the file shows up twice.
  1164        temp_filename = temp_filename.strip().replace('\\', '/')
  1165        if temp_filename != filename:
  1166          # File has property changes but no modifications, create a new diff.
  1167          new_filename = temp_filename
  1168      if new_filename:
  1169        if filename and diff:
  1170          patches.append((filename, ''.join(diff)))
  1171        filename = new_filename
  1172        diff = [line]
  1173        continue
  1174      if diff is not None:
  1175        diff.append(line)
  1176    if filename and diff:
  1177      patches.append((filename, ''.join(diff)))
  1178    return patches
  1179  
  1180  
  1181  def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
  1182    """Uploads a separate patch for each file in the diff output.
  1183  
  1184    Returns a list of [patch_key, filename] for each file.
  1185    """
  1186    patches = SplitPatch(data)
  1187    rv = []
  1188    for patch in patches:
  1189      if len(patch[1]) > MAX_UPLOAD_SIZE:
  1190        print ("Not uploading the patch for " + patch[0] +
  1191               " because the file is too large.")
  1192        continue
  1193      form_fields = [("filename", patch[0])]
  1194      if not options.download_base:
  1195        form_fields.append(("content_upload", "1"))
  1196      files = [("data", "data.diff", patch[1])]
  1197      ctype, body = EncodeMultipartFormData(form_fields, files)
  1198      url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
  1199      print "Uploading patch for " + patch[0]
  1200      response_body = rpc_server.Send(url, body, content_type=ctype)
  1201      lines = response_body.splitlines()
  1202      if not lines or lines[0] != "OK":
  1203        StatusUpdate("  --> %s" % response_body)
  1204        sys.exit(1)
  1205      rv.append([lines[1], patch[0]])
  1206    return rv
  1207  
  1208  
  1209  def GuessVCS(options):
  1210    """Helper to guess the version control system.
  1211  
  1212    This examines the current directory, guesses which VersionControlSystem
  1213    we're using, and returns an instance of the appropriate class.  Exit with an
  1214    error if we can't figure it out.
  1215  
  1216    Returns:
  1217      A VersionControlSystem instance. Exits if the VCS can't be guessed.
  1218    """
  1219    # Mercurial has a command to get the base directory of a repository
  1220    # Try running it, but don't die if we don't have hg installed.
  1221    # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
  1222    try:
  1223      out, returncode = RunShellWithReturnCode(["hg", "root"])
  1224      if returncode == 0:
  1225        return MercurialVCS(options, out.strip())
  1226    except OSError, (errno, message):
  1227      if errno != 2:  # ENOENT -- they don't have hg installed.
  1228        raise
  1229  
  1230    # Subversion has a .svn in all working directories.
  1231    if os.path.isdir('.svn'):
  1232      logging.info("Guessed VCS = Subversion")
  1233      return SubversionVCS(options)
  1234  
  1235    # Git has a command to test if you're in a git tree.
  1236    # Try running it, but don't die if we don't have git installed.
  1237    try:
  1238      out, returncode = RunShellWithReturnCode(["git", "rev-parse",
  1239                                                "--is-inside-work-tree"])
  1240      if returncode == 0:
  1241        return GitVCS(options)
  1242    except OSError, (errno, message):
  1243      if errno != 2:  # ENOENT -- they don't have git installed.
  1244        raise
  1245  
  1246    ErrorExit(("Could not guess version control system. "
  1247               "Are you in a working copy directory?"))
  1248  
  1249  
  1250  def RealMain(argv, data=None):
  1251    """The real main function.
  1252  
  1253    Args:
  1254      argv: Command line arguments.
  1255      data: Diff contents. If None (default) the diff is generated by
  1256        the VersionControlSystem implementation returned by GuessVCS().
  1257  
  1258    Returns:
  1259      A 2-tuple (issue id, patchset id).
  1260      The patchset id is None if the base files are not uploaded by this
  1261      script (applies only to SVN checkouts).
  1262    """
  1263    logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
  1264                                "%(lineno)s %(message)s "))
  1265    os.environ['LC_ALL'] = 'C'
  1266    options, args = parser.parse_args(argv[1:])
  1267    global verbosity
  1268    verbosity = options.verbose
  1269    if verbosity >= 3:
  1270      logging.getLogger().setLevel(logging.DEBUG)
  1271    elif verbosity >= 2:
  1272      logging.getLogger().setLevel(logging.INFO)
  1273    vcs = GuessVCS(options)
  1274    if isinstance(vcs, SubversionVCS):
  1275      # base field is only allowed for Subversion.
  1276      # Note: Fetching base files may become deprecated in future releases.
  1277      base = vcs.GuessBase(options.download_base)
  1278    else:
  1279      base = None
  1280    if not base and options.download_base:
  1281      options.download_base = True
  1282      logging.info("Enabled upload of base file")
  1283    if not options.assume_yes:
  1284      vcs.CheckForUnknownFiles()
  1285    if data is None:
  1286      data = vcs.GenerateDiff(args)
  1287    files = vcs.GetBaseFiles(data)
  1288    if verbosity >= 1:
  1289      print "Upload server:", options.server, "(change with -s/--server)"
  1290    if options.issue:
  1291      prompt = "Message describing this patch set: "
  1292    else:
  1293      prompt = "New issue subject: "
  1294    message = options.message or raw_input(prompt).strip()
  1295    if not message:
  1296      ErrorExit("A non-empty message is required")
  1297    rpc_server = GetRpcServer(options)
  1298    form_fields = [("subject", message)]
  1299    if base:
  1300      form_fields.append(("base", base))
  1301    if options.issue:
  1302      form_fields.append(("issue", str(options.issue)))
  1303    if options.email:
  1304      form_fields.append(("user", options.email))
  1305    if options.reviewers:
  1306      for reviewer in options.reviewers.split(','):
  1307        if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1:
  1308          ErrorExit("Invalid email address: %s" % reviewer)
  1309      form_fields.append(("reviewers", options.reviewers))
  1310    if options.cc:
  1311      for cc in options.cc.split(','):
  1312        if "@" in cc and not cc.split("@")[1].count(".") == 1:
  1313          ErrorExit("Invalid email address: %s" % cc)
  1314      form_fields.append(("cc", options.cc))
  1315    description = options.description
  1316    if options.description_file:
  1317      if options.description:
  1318        ErrorExit("Can't specify description and description_file")
  1319      file = open(options.description_file, 'r')
  1320      description = file.read()
  1321      file.close()
  1322    if description:
  1323      form_fields.append(("description", description))
  1324    # Send a hash of all the base file so the server can determine if a copy
  1325    # already exists in an earlier patchset.
  1326    base_hashes = ""
  1327    for file, info in files.iteritems():
  1328      if not info[0] is None:
  1329        checksum = md5.new(info[0]).hexdigest()
  1330        if base_hashes:
  1331          base_hashes += "|"
  1332        base_hashes += checksum + ":" + file
  1333    form_fields.append(("base_hashes", base_hashes))
  1334    # If we're uploading base files, don't send the email before the uploads, so
  1335    # that it contains the file status.
  1336    if options.send_mail and options.download_base:
  1337      form_fields.append(("send_mail", "1"))
  1338    if not options.download_base:
  1339      form_fields.append(("content_upload", "1"))
  1340    if len(data) > MAX_UPLOAD_SIZE:
  1341      print "Patch is large, so uploading file patches separately."
  1342      uploaded_diff_file = []
  1343      form_fields.append(("separate_patches", "1"))
  1344    else:
  1345      uploaded_diff_file = [("data", "data.diff", data)]
  1346    ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
  1347    response_body = rpc_server.Send("/upload", body, content_type=ctype)
  1348    patchset = None
  1349    if not options.download_base or not uploaded_diff_file:
  1350      lines = response_body.splitlines()
  1351      if len(lines) >= 2:
  1352        msg = lines[0]
  1353        patchset = lines[1].strip()
  1354        patches = [x.split(" ", 1) for x in lines[2:]]
  1355      else:
  1356        msg = response_body
  1357    else:
  1358      msg = response_body
  1359    StatusUpdate(msg)
  1360    if not response_body.startswith("Issue created.") and \
  1361    not response_body.startswith("Issue updated."):
  1362      sys.exit(0)
  1363    issue = msg[msg.rfind("/")+1:]
  1364  
  1365    if not uploaded_diff_file:
  1366      result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
  1367      if not options.download_base:
  1368        patches = result
  1369  
  1370    if not options.download_base:
  1371      vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
  1372      if options.send_mail:
  1373        rpc_server.Send("/" + issue + "/mail", payload="")
  1374    return issue, patchset
  1375  
  1376  
  1377  def main():
  1378    try:
  1379      RealMain(sys.argv)
  1380    except KeyboardInterrupt:
  1381      print
  1382      StatusUpdate("Interrupted.")
  1383      sys.exit(1)
  1384  
  1385  
  1386  if __name__ == "__main__":
  1387    main()