Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2025-08-06 08:19:54

0001 #!/usr/bin/env python
0002 #
0003 # Copyright 2007 Google Inc.
0004 #
0005 # Licensed under the Apache License, Version 2.0 (the "License");
0006 # you may not use this file except in compliance with the License.
0007 # You may obtain a copy of the License at
0008 #
0009 #     http://www.apache.org/licenses/LICENSE-2.0
0010 #
0011 # Unless required by applicable law or agreed to in writing, software
0012 # distributed under the License is distributed on an "AS IS" BASIS,
0013 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014 # See the License for the specific language governing permissions and
0015 # limitations under the License.
0016 
0017 """Tool for uploading diffs from a version control system to the codereview app.
0018 
0019 Usage summary: upload.py [options] [-- diff_options]
0020 
0021 Diff options are passed to the diff command of the underlying system.
0022 
0023 Supported version control systems:
0024   Git
0025   Mercurial
0026   Subversion
0027 
0028 It is important for Git/Mercurial users to specify a tree/node/branch to diff
0029 against by using the '--rev' option.
0030 """
0031 # This code is derived from appcfg.py in the App Engine SDK (open source),
0032 # and from ASPN recipe #146306.
0033 
0034 import cookielib
0035 import getpass
0036 import logging
0037 import md5
0038 import mimetypes
0039 import optparse
0040 import os
0041 import re
0042 import socket
0043 import subprocess
0044 import sys
0045 import urllib
0046 import urllib2
0047 import urlparse
0048 
0049 try:
0050   import readline
0051 except ImportError:
0052   pass
0053 
0054 # The logging verbosity:
0055 #  0: Errors only.
0056 #  1: Status messages.
0057 #  2: Info logs.
0058 #  3: Debug logs.
0059 verbosity = 1
0060 
0061 # Max size of patch or base file.
0062 MAX_UPLOAD_SIZE = 900 * 1024
0063 
0064 
0065 def GetEmail(prompt):
0066   """Prompts the user for their email address and returns it.
0067 
0068   The last used email address is saved to a file and offered up as a suggestion
0069   to the user. If the user presses enter without typing in anything the last
0070   used email address is used. If the user enters a new address, it is saved
0071   for next time we prompt.
0072 
0073   """
0074   last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
0075   last_email = ""
0076   if os.path.exists(last_email_file_name):
0077     try:
0078       last_email_file = open(last_email_file_name, "r")
0079       last_email = last_email_file.readline().strip("\n")
0080       last_email_file.close()
0081       prompt += " [%s]" % last_email
0082     except IOError, e:
0083       pass
0084   email = raw_input(prompt + ": ").strip()
0085   if email:
0086     try:
0087       last_email_file = open(last_email_file_name, "w")
0088       last_email_file.write(email)
0089       last_email_file.close()
0090     except IOError, e:
0091       pass
0092   else:
0093     email = last_email
0094   return email
0095 
0096 
0097 def StatusUpdate(msg):
0098   """Print a status message to stdout.
0099 
0100   If 'verbosity' is greater than 0, print the message.
0101 
0102   Args:
0103     msg: The string to print.
0104   """
0105   if verbosity > 0:
0106     print msg
0107 
0108 
0109 def ErrorExit(msg):
0110   """Print an error message to stderr and exit."""
0111   print >>sys.stderr, msg
0112   sys.exit(1)
0113 
0114 
0115 class ClientLoginError(urllib2.HTTPError):
0116   """Raised to indicate there was an error authenticating with ClientLogin."""
0117 
0118   def __init__(self, url, code, msg, headers, args):
0119     urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
0120     self.args = args
0121     self.reason = args["Error"]
0122 
0123 
0124 class AbstractRpcServer(object):
0125   """Provides a common interface for a simple RPC server."""
0126 
0127   def __init__(self, host, auth_function, host_override=None, extra_headers={},
0128                save_cookies=False):
0129     """Creates a new HttpRpcServer.
0130 
0131     Args:
0132       host: The host to send requests to.
0133       auth_function: A function that takes no arguments and returns an
0134         (email, password) tuple when called. Will be called if authentication
0135         is required.
0136       host_override: The host header to send to the server (defaults to host).
0137       extra_headers: A dict of extra headers to append to every request.
0138       save_cookies: If True, save the authentication cookies to local disk.
0139         If False, use an in-memory cookiejar instead.  Subclasses must
0140         implement this functionality.  Defaults to False.
0141     """
0142     self.host = host
0143     self.host_override = host_override
0144     self.auth_function = auth_function
0145     self.authenticated = False
0146     self.extra_headers = extra_headers
0147     self.save_cookies = save_cookies
0148     self.opener = self._GetOpener()
0149     if self.host_override:
0150       logging.info("Server: %s; Host: %s", self.host, self.host_override)
0151     else:
0152       logging.info("Server: %s", self.host)
0153 
0154   def _GetOpener(self):
0155     """Returns an OpenerDirector for making HTTP requests.
0156 
0157     Returns:
0158       A urllib2.OpenerDirector object.
0159     """
0160     raise NotImplementedError()
0161 
0162   def _CreateRequest(self, url, data=None):
0163     """Creates a new urllib request."""
0164     logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
0165     req = urllib2.Request(url, data=data)
0166     if self.host_override:
0167       req.add_header("Host", self.host_override)
0168     for key, value in self.extra_headers.iteritems():
0169       req.add_header(key, value)
0170     return req
0171 
0172   def _GetAuthToken(self, email, password):
0173     """Uses ClientLogin to authenticate the user, returning an auth token.
0174 
0175     Args:
0176       email:    The user's email address
0177       password: The user's password
0178 
0179     Raises:
0180       ClientLoginError: If there was an error authenticating with ClientLogin.
0181       HTTPError: If there was some other form of HTTP error.
0182 
0183     Returns:
0184       The authentication token returned by ClientLogin.
0185     """
0186     account_type = "GOOGLE"
0187     if self.host.endswith(".google.com"):
0188       # Needed for use inside Google.
0189       account_type = "HOSTED"
0190     req = self._CreateRequest(
0191         url="https://www.google.com/accounts/ClientLogin",
0192         data=urllib.urlencode({
0193             "Email": email,
0194             "Passwd": password,
0195             "service": "ah",
0196             "source": "rietveld-codereview-upload",
0197             "accountType": account_type,
0198         }),
0199     )
0200     try:
0201       response = self.opener.open(req)
0202       response_body = response.read()
0203       response_dict = dict(x.split("=")
0204                            for x in response_body.split("\n") if x)
0205       return response_dict["Auth"]
0206     except urllib2.HTTPError, e:
0207       if e.code == 403:
0208         body = e.read()
0209         response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
0210         raise ClientLoginError(req.get_full_url(), e.code, e.msg,
0211                                e.headers, response_dict)
0212       else:
0213         raise
0214 
0215   def _GetAuthCookie(self, auth_token):
0216     """Fetches authentication cookies for an authentication token.
0217 
0218     Args:
0219       auth_token: The authentication token returned by ClientLogin.
0220 
0221     Raises:
0222       HTTPError: If there was an error fetching the authentication cookies.
0223     """
0224     # This is a dummy value to allow us to identify when we're successful.
0225     continue_location = "http://localhost/"
0226     args = {"continue": continue_location, "auth": auth_token}
0227     req = self._CreateRequest("http://%s/_ah/login?%s" %
0228                               (self.host, urllib.urlencode(args)))
0229     try:
0230       response = self.opener.open(req)
0231     except urllib2.HTTPError, e:
0232       response = e
0233     if (response.code != 302 or
0234         response.info()["location"] != continue_location):
0235       raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
0236                               response.headers, response.fp)
0237     self.authenticated = True
0238 
0239   def _Authenticate(self):
0240     """Authenticates the user.
0241 
0242     The authentication process works as follows:
0243      1) We get a username and password from the user
0244      2) We use ClientLogin to obtain an AUTH token for the user
0245         (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
0246      3) We pass the auth token to /_ah/login on the server to obtain an
0247         authentication cookie. If login was successful, it tries to redirect
0248         us to the URL we provided.
0249 
0250     If we attempt to access the upload API without first obtaining an
0251     authentication cookie, it returns a 401 response and directs us to
0252     authenticate ourselves with ClientLogin.
0253     """
0254     for i in range(3):
0255       credentials = self.auth_function()
0256       try:
0257         auth_token = self._GetAuthToken(credentials[0], credentials[1])
0258       except ClientLoginError, e:
0259         if e.reason == "BadAuthentication":
0260           print >>sys.stderr, "Invalid username or password."
0261           continue
0262         if e.reason == "CaptchaRequired":
0263           print >>sys.stderr, (
0264               "Please go to\n"
0265               "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
0266               "and verify you are a human.  Then try again.")
0267           break
0268         if e.reason == "NotVerified":
0269           print >>sys.stderr, "Account not verified."
0270           break
0271         if e.reason == "TermsNotAgreed":
0272           print >>sys.stderr, "User has not agreed to TOS."
0273           break
0274         if e.reason == "AccountDeleted":
0275           print >>sys.stderr, "The user account has been deleted."
0276           break
0277         if e.reason == "AccountDisabled":
0278           print >>sys.stderr, "The user account has been disabled."
0279           break
0280         if e.reason == "ServiceDisabled":
0281           print >>sys.stderr, ("The user's access to the service has been "
0282                                "disabled.")
0283           break
0284         if e.reason == "ServiceUnavailable":
0285           print >>sys.stderr, "The service is not available; try again later."
0286           break
0287         raise
0288       self._GetAuthCookie(auth_token)
0289       return
0290 
0291   def Send(self, request_path, payload=None,
0292            content_type="application/octet-stream",
0293            timeout=None,
0294            **kwargs):
0295     """Sends an RPC and returns the response.
0296 
0297     Args:
0298       request_path: The path to send the request to, eg /api/appversion/create.
0299       payload: The body of the request, or None to send an empty request.
0300       content_type: The Content-Type header to use.
0301       timeout: timeout in seconds; default None i.e. no timeout.
0302         (Note: for large requests on OS X, the timeout doesn't work right.)
0303       kwargs: Any keyword arguments are converted into query string parameters.
0304 
0305     Returns:
0306       The response body, as a string.
0307     """
0308     # TODO: Don't require authentication.  Let the server say
0309     # whether it is necessary.
0310     if not self.authenticated:
0311       self._Authenticate()
0312 
0313     old_timeout = socket.getdefaulttimeout()
0314     socket.setdefaulttimeout(timeout)
0315     try:
0316       tries = 0
0317       while True:
0318         tries += 1
0319         args = dict(kwargs)
0320         url = "http://%s%s" % (self.host, request_path)
0321         if args:
0322           url += "?" + urllib.urlencode(args)
0323         req = self._CreateRequest(url=url, data=payload)
0324         req.add_header("Content-Type", content_type)
0325         try:
0326           f = self.opener.open(req)
0327           response = f.read()
0328           f.close()
0329           return response
0330         except urllib2.HTTPError, e:
0331           if tries > 3:
0332             raise
0333           elif e.code == 401:
0334             self._Authenticate()
0335 ##           elif e.code >= 500 and e.code < 600:
0336 ##             # Server Error - try again.
0337 ##             continue
0338           else:
0339             raise
0340     finally:
0341       socket.setdefaulttimeout(old_timeout)
0342 
0343 
0344 class HttpRpcServer(AbstractRpcServer):
0345   """Provides a simplified RPC-style interface for HTTP requests."""
0346 
0347   def _Authenticate(self):
0348     """Save the cookie jar after authentication."""
0349     super(HttpRpcServer, self)._Authenticate()
0350     if self.save_cookies:
0351       StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
0352       self.cookie_jar.save()
0353 
0354   def _GetOpener(self):
0355     """Returns an OpenerDirector that supports cookies and ignores redirects.
0356 
0357     Returns:
0358       A urllib2.OpenerDirector object.
0359     """
0360     opener = urllib2.OpenerDirector()
0361     opener.add_handler(urllib2.ProxyHandler())
0362     opener.add_handler(urllib2.UnknownHandler())
0363     opener.add_handler(urllib2.HTTPHandler())
0364     opener.add_handler(urllib2.HTTPDefaultErrorHandler())
0365     opener.add_handler(urllib2.HTTPSHandler())
0366     opener.add_handler(urllib2.HTTPErrorProcessor())
0367     if self.save_cookies:
0368       self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
0369       self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
0370       if os.path.exists(self.cookie_file):
0371         try:
0372           self.cookie_jar.load()
0373           self.authenticated = True
0374           StatusUpdate("Loaded authentication cookies from %s" %
0375                        self.cookie_file)
0376         except (cookielib.LoadError, IOError):
0377           # Failed to load cookies - just ignore them.
0378           pass
0379       else:
0380         # Create an empty cookie file with mode 600
0381         fd = os.open(self.cookie_file, os.O_CREAT, 0600)
0382         os.close(fd)
0383       # Always chmod the cookie file
0384       os.chmod(self.cookie_file, 0600)
0385     else:
0386       # Don't save cookies across runs of update.py.
0387       self.cookie_jar = cookielib.CookieJar()
0388     opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
0389     return opener
0390 
0391 
0392 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
0393 parser.add_option("-y", "--assume_yes", action="store_true",
0394                   dest="assume_yes", default=False,
0395                   help="Assume that the answer to yes/no questions is 'yes'.")
0396 # Logging
0397 group = parser.add_option_group("Logging options")
0398 group.add_option("-q", "--quiet", action="store_const", const=0,
0399                  dest="verbose", help="Print errors only.")
0400 group.add_option("-v", "--verbose", action="store_const", const=2,
0401                  dest="verbose", default=1,
0402                  help="Print info level logs (default).")
0403 group.add_option("--noisy", action="store_const", const=3,
0404                  dest="verbose", help="Print all logs.")
0405 # Review server
0406 group = parser.add_option_group("Review server options")
0407 group.add_option("-s", "--server", action="store", dest="server",
0408                  default="codereview.appspot.com",
0409                  metavar="SERVER",
0410                  help=("The server to upload to. The format is host[:port]. "
0411                        "Defaults to 'codereview.appspot.com'."))
0412 group.add_option("-e", "--email", action="store", dest="email",
0413                  metavar="EMAIL", default=None,
0414                  help="The username to use. Will prompt if omitted.")
0415 group.add_option("-H", "--host", action="store", dest="host",
0416                  metavar="HOST", default=None,
0417                  help="Overrides the Host header sent with all RPCs.")
0418 group.add_option("--no_cookies", action="store_false",
0419                  dest="save_cookies", default=True,
0420                  help="Do not save authentication cookies to local disk.")
0421 # Issue
0422 group = parser.add_option_group("Issue options")
0423 group.add_option("-d", "--description", action="store", dest="description",
0424                  metavar="DESCRIPTION", default=None,
0425                  help="Optional description when creating an issue.")
0426 group.add_option("-f", "--description_file", action="store",
0427                  dest="description_file", metavar="DESCRIPTION_FILE",
0428                  default=None,
0429                  help="Optional path of a file that contains "
0430                       "the description when creating an issue.")
0431 group.add_option("-r", "--reviewers", action="store", dest="reviewers",
0432                  metavar="REVIEWERS", default=None,
0433                  help="Add reviewers (comma separated email addresses).")
0434 group.add_option("--cc", action="store", dest="cc",
0435                  metavar="CC", default=None,
0436                  help="Add CC (comma separated email addresses).")
0437 # Upload options
0438 group = parser.add_option_group("Patch options")
0439 group.add_option("-m", "--message", action="store", dest="message",
0440                  metavar="MESSAGE", default=None,
0441                  help="A message to identify the patch. "
0442                       "Will prompt if omitted.")
0443 group.add_option("-i", "--issue", type="int", action="store",
0444                  metavar="ISSUE", default=None,
0445                  help="Issue number to which to add. Defaults to new issue.")
0446 group.add_option("--download_base", action="store_true",
0447                  dest="download_base", default=False,
0448                  help="Base files will be downloaded by the server "
0449                  "(side-by-side diffs may not work on files with CRs).")
0450 group.add_option("--rev", action="store", dest="revision",
0451                  metavar="REV", default=None,
0452                  help="Branch/tree/revision to diff against (used by DVCS).")
0453 group.add_option("--send_mail", action="store_true",
0454                  dest="send_mail", default=False,
0455                  help="Send notification email to reviewers.")
0456 
0457 
0458 def GetRpcServer(options):
0459   """Returns an instance of an AbstractRpcServer.
0460 
0461   Returns:
0462     A new AbstractRpcServer, on which RPC calls can be made.
0463   """
0464 
0465   rpc_server_class = HttpRpcServer
0466 
0467   def GetUserCredentials():
0468     """Prompts the user for a username and password."""
0469     email = options.email
0470     if email is None:
0471       email = GetEmail("Email (login for uploading to %s)" % options.server)
0472     password = getpass.getpass("Password for %s: " % email)
0473     return (email, password)
0474 
0475   # If this is the dev_appserver, use fake authentication.
0476   host = (options.host or options.server).lower()
0477   if host == "localhost" or host.startswith("localhost:"):
0478     email = options.email
0479     if email is None:
0480       email = "test@example.com"
0481       logging.info("Using debug user %s.  Override with --email" % email)
0482     server = rpc_server_class(
0483         options.server,
0484         lambda: (email, "password"),
0485         host_override=options.host,
0486         extra_headers={"Cookie":
0487                        'dev_appserver_login="%s:False"' % email},
0488         save_cookies=options.save_cookies)
0489     # Don't try to talk to ClientLogin.
0490     server.authenticated = True
0491     return server
0492 
0493   return rpc_server_class(options.server, GetUserCredentials,
0494                           host_override=options.host,
0495                           save_cookies=options.save_cookies)
0496 
0497 
0498 def EncodeMultipartFormData(fields, files):
0499   """Encode form fields for multipart/form-data.
0500 
0501   Args:
0502     fields: A sequence of (name, value) elements for regular form fields.
0503     files: A sequence of (name, filename, value) elements for data to be
0504            uploaded as files.
0505   Returns:
0506     (content_type, body) ready for httplib.HTTP instance.
0507 
0508   Source:
0509     http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
0510   """
0511   BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
0512   CRLF = '\r\n'
0513   lines = []
0514   for (key, value) in fields:
0515     lines.append('--' + BOUNDARY)
0516     lines.append('Content-Disposition: form-data; name="%s"' % key)
0517     lines.append('')
0518     lines.append(value)
0519   for (key, filename, value) in files:
0520     lines.append('--' + BOUNDARY)
0521     lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
0522              (key, filename))
0523     lines.append('Content-Type: %s' % GetContentType(filename))
0524     lines.append('')
0525     lines.append(value)
0526   lines.append('--' + BOUNDARY + '--')
0527   lines.append('')
0528   body = CRLF.join(lines)
0529   content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
0530   return content_type, body
0531 
0532 
0533 def GetContentType(filename):
0534   """Helper to guess the content-type from the filename."""
0535   return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
0536 
0537 
0538 # Use a shell for subcommands on Windows to get a PATH search.
0539 use_shell = sys.platform.startswith("win")
0540 
0541 def RunShellWithReturnCode(command, print_output=False,
0542                            universal_newlines=True):
0543   """Executes a command and returns the output from stdout and the return code.
0544 
0545   Args:
0546     command: Command to execute.
0547     print_output: If True, the output is printed to stdout.
0548                   If False, both stdout and stderr are ignored.
0549     universal_newlines: Use universal_newlines flag (default: True).
0550 
0551   Returns:
0552     Tuple (output, return code)
0553   """
0554   logging.info("Running %s", command)
0555   p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
0556                        shell=use_shell, universal_newlines=universal_newlines)
0557   if print_output:
0558     output_array = []
0559     while True:
0560       line = p.stdout.readline()
0561       if not line:
0562         break
0563       print line.strip("\n")
0564       output_array.append(line)
0565     output = "".join(output_array)
0566   else:
0567     output = p.stdout.read()
0568   p.wait()
0569   errout = p.stderr.read()
0570   if print_output and errout:
0571     print >>sys.stderr, errout
0572   p.stdout.close()
0573   p.stderr.close()
0574   return output, p.returncode
0575 
0576 
0577 def RunShell(command, silent_ok=False, universal_newlines=True,
0578              print_output=False):
0579   data, retcode = RunShellWithReturnCode(command, print_output,
0580                                          universal_newlines)
0581   if retcode:
0582     ErrorExit("Got error status from %s:\n%s" % (command, data))
0583   if not silent_ok and not data:
0584     ErrorExit("No output from %s" % command)
0585   return data
0586 
0587 
0588 class VersionControlSystem(object):
0589   """Abstract base class providing an interface to the VCS."""
0590 
0591   def __init__(self, options):
0592     """Constructor.
0593 
0594     Args:
0595       options: Command line options.
0596     """
0597     self.options = options
0598 
0599   def GenerateDiff(self, args):
0600     """Return the current diff as a string.
0601 
0602     Args:
0603       args: Extra arguments to pass to the diff command.
0604     """
0605     raise NotImplementedError(
0606         "abstract method -- subclass %s must override" % self.__class__)
0607 
0608   def GetUnknownFiles(self):
0609     """Return a list of files unknown to the VCS."""
0610     raise NotImplementedError(
0611         "abstract method -- subclass %s must override" % self.__class__)
0612 
0613   def CheckForUnknownFiles(self):
0614     """Show an "are you sure?" prompt if there are unknown files."""
0615     unknown_files = self.GetUnknownFiles()
0616     if unknown_files:
0617       print "The following files are not added to version control:"
0618       for line in unknown_files:
0619         print line
0620       prompt = "Are you sure to continue?(y/N) "
0621       answer = raw_input(prompt).strip()
0622       if answer != "y":
0623         ErrorExit("User aborted")
0624 
0625   def GetBaseFile(self, filename):
0626     """Get the content of the upstream version of a file.
0627 
0628     Returns:
0629       A tuple (base_content, new_content, is_binary, status)
0630         base_content: The contents of the base file.
0631         new_content: For text files, this is empty.  For binary files, this is
0632           the contents of the new file, since the diff output won't contain
0633           information to reconstruct the current file.
0634         is_binary: True iff the file is binary.
0635         status: The status of the file.
0636     """
0637 
0638     raise NotImplementedError(
0639         "abstract method -- subclass %s must override" % self.__class__)
0640 
0641 
0642   def GetBaseFiles(self, diff):
0643     """Helper that calls GetBase file for each file in the patch.
0644 
0645     Returns:
0646       A dictionary that maps from filename to GetBaseFile's tuple.  Filenames
0647       are retrieved based on lines that start with "Index:" or
0648       "Property changes on:".
0649     """
0650     files = {}
0651     for line in diff.splitlines(True):
0652       if line.startswith('Index:') or line.startswith('Property changes on:'):
0653         unused, filename = line.split(':', 1)
0654         # On Windows if a file has property changes its filename uses '\'
0655         # instead of '/'.
0656         filename = filename.strip().replace('\\', '/')
0657         files[filename] = self.GetBaseFile(filename)
0658     return files
0659 
0660 
0661   def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
0662                       files):
0663     """Uploads the base files (and if necessary, the current ones as well)."""
0664 
0665     def UploadFile(filename, file_id, content, is_binary, status, is_base):
0666       """Uploads a file to the server."""
0667       file_too_large = False
0668       if is_base:
0669         type = "base"
0670       else:
0671         type = "current"
0672       if len(content) > MAX_UPLOAD_SIZE:
0673         print ("Not uploading the %s file for %s because it's too large." %
0674                (type, filename))
0675         file_too_large = True
0676         content = ""
0677       checksum = md5.new(content).hexdigest()
0678       if options.verbose > 0 and not file_too_large:
0679         print "Uploading %s file for %s" % (type, filename)
0680       url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
0681       form_fields = [("filename", filename),
0682                      ("status", status),
0683                      ("checksum", checksum),
0684                      ("is_binary", str(is_binary)),
0685                      ("is_current", str(not is_base)),
0686                     ]
0687       if file_too_large:
0688         form_fields.append(("file_too_large", "1"))
0689       if options.email:
0690         form_fields.append(("user", options.email))
0691       ctype, body = EncodeMultipartFormData(form_fields,
0692                                             [("data", filename, content)])
0693       response_body = rpc_server.Send(url, body,
0694                                       content_type=ctype)
0695       if not response_body.startswith("OK"):
0696         StatusUpdate("  --> %s" % response_body)
0697         sys.exit(1)
0698 
0699     patches = dict()
0700     [patches.setdefault(v, k) for k, v in patch_list]
0701     for filename in patches.keys():
0702       base_content, new_content, is_binary, status = files[filename]
0703       file_id_str = patches.get(filename)
0704       if file_id_str.find("nobase") != -1:
0705         base_content = None
0706         file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
0707       file_id = int(file_id_str)
0708       if base_content != None:
0709         UploadFile(filename, file_id, base_content, is_binary, status, True)
0710       if new_content != None:
0711         UploadFile(filename, file_id, new_content, is_binary, status, False)
0712 
0713   def IsImage(self, filename):
0714     """Returns true if the filename has an image extension."""
0715     mimetype =  mimetypes.guess_type(filename)[0]
0716     if not mimetype:
0717       return False
0718     return mimetype.startswith("image/")
0719 
0720 
0721 class SubversionVCS(VersionControlSystem):
0722   """Implementation of the VersionControlSystem interface for Subversion."""
0723 
0724   def __init__(self, options):
0725     super(SubversionVCS, self).__init__(options)
0726     if self.options.revision:
0727       match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
0728       if not match:
0729         ErrorExit("Invalid Subversion revision %s." % self.options.revision)
0730       self.rev_start = match.group(1)
0731       self.rev_end = match.group(3)
0732     else:
0733       self.rev_start = self.rev_end = None
0734     # Cache output from "svn list -r REVNO dirname".
0735     # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
0736     self.svnls_cache = {}
0737     # SVN base URL is required to fetch files deleted in an older revision.
0738     # Result is cached to not guess it over and over again in GetBaseFile().
0739     required = self.options.download_base or self.options.revision is not None
0740     self.svn_base = self._GuessBase(required)
0741 
0742   def GuessBase(self, required):
0743     """Wrapper for _GuessBase."""
0744     return self.svn_base
0745 
0746   def _GuessBase(self, required):
0747     """Returns the SVN base URL.
0748 
0749     Args:
0750       required: If true, exits if the url can't be guessed, otherwise None is
0751         returned.
0752     """
0753     info = RunShell(["svn", "info"])
0754     for line in info.splitlines():
0755       words = line.split()
0756       if len(words) == 2 and words[0] == "URL:":
0757         url = words[1]
0758         scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
0759         username, netloc = urllib.splituser(netloc)
0760         if username:
0761           logging.info("Removed username from base URL")
0762         if netloc.endswith("svn.python.org"):
0763           if netloc == "svn.python.org":
0764             if path.startswith("/projects/"):
0765               path = path[9:]
0766           elif netloc != "pythondev@svn.python.org":
0767             ErrorExit("Unrecognized Python URL: %s" % url)
0768           base = "http://svn.python.org/view/*checkout*%s/" % path
0769           logging.info("Guessed Python base = %s", base)
0770         elif netloc.endswith("svn.collab.net"):
0771           if path.startswith("/repos/"):
0772             path = path[6:]
0773           base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
0774           logging.info("Guessed CollabNet base = %s", base)
0775         elif netloc.endswith(".googlecode.com"):
0776           path = path + "/"
0777           base = urlparse.urlunparse(("http", netloc, path, params,
0778                                       query, fragment))
0779           logging.info("Guessed Google Code base = %s", base)
0780         else:
0781           path = path + "/"
0782           base = urlparse.urlunparse((scheme, netloc, path, params,
0783                                       query, fragment))
0784           logging.info("Guessed base = %s", base)
0785         return base
0786     if required:
0787       ErrorExit("Can't find URL in output from svn info")
0788     return None
0789 
0790   def GenerateDiff(self, args):
0791     cmd = ["svn", "diff"]
0792     if self.options.revision:
0793       cmd += ["-r", self.options.revision]
0794     cmd.extend(args)
0795     data = RunShell(cmd)
0796     count = 0
0797     for line in data.splitlines():
0798       if line.startswith("Index:") or line.startswith("Property changes on:"):
0799         count += 1
0800         logging.info(line)
0801     if not count:
0802       ErrorExit("No valid patches found in output from svn diff")
0803     return data
0804 
0805   def _CollapseKeywords(self, content, keyword_str):
0806     """Collapses SVN keywords."""
0807     # svn cat translates keywords but svn diff doesn't. As a result of this
0808     # behavior patching.PatchChunks() fails with a chunk mismatch error.
0809     # This part was originally written by the Review Board development team
0810     # who had the same problem (http://reviews.review-board.org/r/276/).
0811     # Mapping of keywords to known aliases
0812     svn_keywords = {
0813       # Standard keywords
0814       'Date':                ['Date', 'LastChangedDate'],
0815       'Revision':            ['Revision', 'LastChangedRevision', 'Rev'],
0816       'Author':              ['Author', 'LastChangedBy'],
0817       'HeadURL':             ['HeadURL', 'URL'],
0818       'Id':                  ['Id'],
0819 
0820       # Aliases
0821       'LastChangedDate':     ['LastChangedDate', 'Date'],
0822       'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
0823       'LastChangedBy':       ['LastChangedBy', 'Author'],
0824       'URL':                 ['URL', 'HeadURL'],
0825     }
0826 
0827     def repl(m):
0828        if m.group(2):
0829          return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
0830        return "$%s$" % m.group(1)
0831     keywords = [keyword
0832                 for name in keyword_str.split(" ")
0833                 for keyword in svn_keywords.get(name, [])]
0834     return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
0835 
0836   def GetUnknownFiles(self):
0837     status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
0838     unknown_files = []
0839     for line in status.split("\n"):
0840       if line and line[0] == "?":
0841         unknown_files.append(line)
0842     return unknown_files
0843 
0844   def ReadFile(self, filename):
0845     """Returns the contents of a file."""
0846     file = open(filename, 'rb')
0847     result = ""
0848     try:
0849       result = file.read()
0850     finally:
0851       file.close()
0852     return result
0853 
0854   def GetStatus(self, filename):
0855     """Returns the status of a file."""
0856     if not self.options.revision:
0857       status = RunShell(["svn", "status", "--ignore-externals", filename])
0858       if not status:
0859         ErrorExit("svn status returned no output for %s" % filename)
0860       status_lines = status.splitlines()
0861       # If file is in a cl, the output will begin with
0862       # "\n--- Changelist 'cl_name':\n".  See
0863       # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
0864       if (len(status_lines) == 3 and
0865           not status_lines[0] and
0866           status_lines[1].startswith("--- Changelist")):
0867         status = status_lines[2]
0868       else:
0869         status = status_lines[0]
0870     # If we have a revision to diff against we need to run "svn list"
0871     # for the old and the new revision and compare the results to get
0872     # the correct status for a file.
0873     else:
0874       dirname, relfilename = os.path.split(filename)
0875       if dirname not in self.svnls_cache:
0876         cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
0877         out, returncode = RunShellWithReturnCode(cmd)
0878         if returncode:
0879           ErrorExit("Failed to get status for %s." % filename)
0880         old_files = out.splitlines()
0881         args = ["svn", "list"]
0882         if self.rev_end:
0883           args += ["-r", self.rev_end]
0884         cmd = args + [dirname or "."]
0885         out, returncode = RunShellWithReturnCode(cmd)
0886         if returncode:
0887           ErrorExit("Failed to run command %s" % cmd)
0888         self.svnls_cache[dirname] = (old_files, out.splitlines())
0889       old_files, new_files = self.svnls_cache[dirname]
0890       if relfilename in old_files and relfilename not in new_files:
0891         status = "D   "
0892       elif relfilename in old_files and relfilename in new_files:
0893         status = "M   "
0894       else:
0895         status = "A   "
0896     return status
0897 
0898   def GetBaseFile(self, filename):
0899     status = self.GetStatus(filename)
0900     base_content = None
0901     new_content = None
0902 
0903     # If a file is copied its status will be "A  +", which signifies
0904     # "addition-with-history".  See "svn st" for more information.  We need to
0905     # upload the original file or else diff parsing will fail if the file was
0906     # edited.
0907     if status[0] == "A" and status[3] != "+":
0908       # We'll need to upload the new content if we're adding a binary file
0909       # since diff's output won't contain it.
0910       mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
0911                           silent_ok=True)
0912       base_content = ""
0913       is_binary = mimetype and not mimetype.startswith("text/")
0914       if is_binary and self.IsImage(filename):
0915         new_content = self.ReadFile(filename)
0916     elif (status[0] in ("M", "D", "R") or
0917           (status[0] == "A" and status[3] == "+") or  # Copied file.
0918           (status[0] == " " and status[1] == "M")):  # Property change.
0919       args = []
0920       if self.options.revision:
0921         url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
0922       else:
0923         # Don't change filename, it's needed later.
0924         url = filename
0925         args += ["-r", "BASE"]
0926       cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
0927       mimetype, returncode = RunShellWithReturnCode(cmd)
0928       if returncode:
0929         # File does not exist in the requested revision.
0930         # Reset mimetype, it contains an error message.
0931         mimetype = ""
0932       get_base = False
0933       is_binary = mimetype and not mimetype.startswith("text/")
0934       if status[0] == " ":
0935         # Empty base content just to force an upload.
0936         base_content = ""
0937       elif is_binary:
0938         if self.IsImage(filename):
0939           get_base = True
0940           if status[0] == "M":
0941             if not self.rev_end:
0942               new_content = self.ReadFile(filename)
0943             else:
0944               url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
0945               new_content = RunShell(["svn", "cat", url],
0946                                      universal_newlines=True, silent_ok=True)
0947         else:
0948           base_content = ""
0949       else:
0950         get_base = True
0951 
0952       if get_base:
0953         if is_binary:
0954           universal_newlines = False
0955         else:
0956           universal_newlines = True
0957         if self.rev_start:
0958           # "svn cat -r REV delete_file.txt" doesn't work. cat requires
0959           # the full URL with "@REV" appended instead of using "-r" option.
0960           url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
0961           base_content = RunShell(["svn", "cat", url],
0962                                   universal_newlines=universal_newlines,
0963                                   silent_ok=True)
0964         else:
0965           base_content = RunShell(["svn", "cat", filename],
0966                                   universal_newlines=universal_newlines,
0967                                   silent_ok=True)
0968         if not is_binary:
0969           args = []
0970           if self.rev_start:
0971             url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
0972           else:
0973             url = filename
0974             args += ["-r", "BASE"]
0975           cmd = ["svn"] + args + ["propget", "svn:keywords", url]
0976           keywords, returncode = RunShellWithReturnCode(cmd)
0977           if keywords and not returncode:
0978             base_content = self._CollapseKeywords(base_content, keywords)
0979     else:
0980       StatusUpdate("svn status returned unexpected output: %s" % status)
0981       sys.exit(1)
0982     return base_content, new_content, is_binary, status[0:5]
0983 
0984 
0985 class GitVCS(VersionControlSystem):
0986   """Implementation of the VersionControlSystem interface for Git."""
0987 
0988   def __init__(self, options):
0989     super(GitVCS, self).__init__(options)
0990     # Map of filename -> hash of base file.
0991     self.base_hashes = {}
0992 
0993   def GenerateDiff(self, extra_args):
0994     # This is more complicated than svn's GenerateDiff because we must convert
0995     # the diff output to include an svn-style "Index:" line as well as record
0996     # the hashes of the base files, so we can upload them along with our diff.
0997     if self.options.revision:
0998       extra_args = [self.options.revision] + extra_args
0999     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()