media/webrtc/trunk/testing/gtest/scripts/upload.py

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rwxr-xr-x

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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.
    17 """Tool for uploading diffs from a version control system to the codereview app.
    19 Usage summary: upload.py [options] [-- diff_options]
    21 Diff options are passed to the diff command of the underlying system.
    23 Supported version control systems:
    24   Git
    25   Mercurial
    26   Subversion
    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.
    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
    49 try:
    50   import readline
    51 except ImportError:
    52   pass
    54 # The logging verbosity:
    55 #  0: Errors only.
    56 #  1: Status messages.
    57 #  2: Info logs.
    58 #  3: Debug logs.
    59 verbosity = 1
    61 # Max size of patch or base file.
    62 MAX_UPLOAD_SIZE = 900 * 1024
    65 def GetEmail(prompt):
    66   """Prompts the user for their email address and returns it.
    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.
    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
    97 def StatusUpdate(msg):
    98   """Print a status message to stdout.
   100   If 'verbosity' is greater than 0, print the message.
   102   Args:
   103     msg: The string to print.
   104   """
   105   if verbosity > 0:
   106     print msg
   109 def ErrorExit(msg):
   110   """Print an error message to stderr and exit."""
   111   print >>sys.stderr, msg
   112   sys.exit(1)
   115 class ClientLoginError(urllib2.HTTPError):
   116   """Raised to indicate there was an error authenticating with ClientLogin."""
   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"]
   124 class AbstractRpcServer(object):
   125   """Provides a common interface for a simple RPC server."""
   127   def __init__(self, host, auth_function, host_override=None, extra_headers={},
   128                save_cookies=False):
   129     """Creates a new HttpRpcServer.
   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)
   154   def _GetOpener(self):
   155     """Returns an OpenerDirector for making HTTP requests.
   157     Returns:
   158       A urllib2.OpenerDirector object.
   159     """
   160     raise NotImplementedError()
   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
   172   def _GetAuthToken(self, email, password):
   173     """Uses ClientLogin to authenticate the user, returning an auth token.
   175     Args:
   176       email:    The user's email address
   177       password: The user's password
   179     Raises:
   180       ClientLoginError: If there was an error authenticating with ClientLogin.
   181       HTTPError: If there was some other form of HTTP error.
   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
   215   def _GetAuthCookie(self, auth_token):
   216     """Fetches authentication cookies for an authentication token.
   218     Args:
   219       auth_token: The authentication token returned by ClientLogin.
   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
   239   def _Authenticate(self):
   240     """Authenticates the user.
   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.
   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
   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.
   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.
   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()
   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)
   344 class HttpRpcServer(AbstractRpcServer):
   345   """Provides a simplified RPC-style interface for HTTP requests."""
   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()
   354   def _GetOpener(self):
   355     """Returns an OpenerDirector that supports cookies and ignores redirects.
   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
   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.")
   458 def GetRpcServer(options):
   459   """Returns an instance of an AbstractRpcServer.
   461   Returns:
   462     A new AbstractRpcServer, on which RPC calls can be made.
   463   """
   465   rpc_server_class = HttpRpcServer
   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)
   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
   493   return rpc_server_class(options.server, GetUserCredentials,
   494                           host_override=options.host,
   495                           save_cookies=options.save_cookies)
   498 def EncodeMultipartFormData(fields, files):
   499   """Encode form fields for multipart/form-data.
   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.
   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
   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'
   538 # Use a shell for subcommands on Windows to get a PATH search.
   539 use_shell = sys.platform.startswith("win")
   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.
   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).
   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
   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
   588 class VersionControlSystem(object):
   589   """Abstract base class providing an interface to the VCS."""
   591   def __init__(self, options):
   592     """Constructor.
   594     Args:
   595       options: Command line options.
   596     """
   597     self.options = options
   599   def GenerateDiff(self, args):
   600     """Return the current diff as a string.
   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__)
   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__)
   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")
   625   def GetBaseFile(self, filename):
   626     """Get the content of the upstream version of a file.
   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     """
   638     raise NotImplementedError(
   639         "abstract method -- subclass %s must override" % self.__class__)
   642   def GetBaseFiles(self, diff):
   643     """Helper that calls GetBase file for each file in the patch.
   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
   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)."""
   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)
   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)
   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/")
   721 class SubversionVCS(VersionControlSystem):
   722   """Implementation of the VersionControlSystem interface for Subversion."""
   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)
   742   def GuessBase(self, required):
   743     """Wrapper for _GuessBase."""
   744     return self.svn_base
   746   def _GuessBase(self, required):
   747     """Returns the SVN base URL.
   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
   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
   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'],
   820       # Aliases
   821       'LastChangedDate':     ['LastChangedDate', 'Date'],
   822       'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
   823       'LastChangedBy':       ['LastChangedBy', 'Author'],
   824       'URL':                 ['URL', 'HeadURL'],
   825     }
   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)
   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
   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
   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
   898   def GetBaseFile(self, filename):
   899     status = self.GetStatus(filename)
   900     base_content = None
   901     new_content = None
   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
   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]
   985 class GitVCS(VersionControlSystem):
   986   """Implementation of the VersionControlSystem interface for Git."""
   988   def __init__(self, options):
   989     super(GitVCS, self).__init__(options)
   990     # Map of filename -> hash of base file.
   991     self.base_hashes = {}
   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)
  1021   def GetUnknownFiles(self):
  1022     status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
  1023                       silent_ok=True)
  1024     return status.splitlines()
  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)
  1042 class MercurialVCS(VersionControlSystem):
  1043   """Implementation of the VersionControlSystem interface for Mercurial."""
  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()
  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"\/")
  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"
  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
  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
  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.
  1144   Args:
  1145     data: A string containing the output of svn diff.
  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
  1181 def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
  1182   """Uploads a separate patch for each file in the diff output.
  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
  1209 def GuessVCS(options):
  1210   """Helper to guess the version control system.
  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.
  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
  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)
  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
  1246   ErrorExit(("Could not guess version control system. "
  1247              "Are you in a working copy directory?"))
  1250 def RealMain(argv, data=None):
  1251   """The real main function.
  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().
  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:]
  1365   if not uploaded_diff_file:
  1366     result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
  1367     if not options.download_base:
  1368       patches = result
  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
  1377 def main():
  1378   try:
  1379     RealMain(sys.argv)
  1380   except KeyboardInterrupt:
  1381     print
  1382     StatusUpdate("Interrupted.")
  1383     sys.exit(1)
  1386 if __name__ == "__main__":
  1387   main()

mercurial