upload.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2007 Google Inc.
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License");
00006 # you may not use this file except in compliance with the License.
00007 # You may obtain a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS,
00013 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
00014 # See the License for the specific language governing permissions and
00015 # limitations under the License.
00016 
00017 """Tool for uploading diffs from a version control system to the codereview app.
00018 
00019 Usage summary: upload.py [options] [-- diff_options]
00020 
00021 Diff options are passed to the diff command of the underlying system.
00022 
00023 Supported version control systems:
00024   Git
00025   Mercurial
00026   Subversion
00027 
00028 It is important for Git/Mercurial users to specify a tree/node/branch to diff
00029 against by using the '--rev' option.
00030 """
00031 # This code is derived from appcfg.py in the App Engine SDK (open source),
00032 # and from ASPN recipe #146306.
00033 
00034 import cookielib
00035 import getpass
00036 import logging
00037 import md5
00038 import mimetypes
00039 import optparse
00040 import os
00041 import re
00042 import socket
00043 import subprocess
00044 import sys
00045 import urllib
00046 import urllib2
00047 import urlparse
00048 
00049 try:
00050   import readline
00051 except ImportError:
00052   pass
00053 
00054 # The logging verbosity:
00055 #  0: Errors only.
00056 #  1: Status messages.
00057 #  2: Info logs.
00058 #  3: Debug logs.
00059 verbosity = 1
00060 
00061 # Max size of patch or base file.
00062 MAX_UPLOAD_SIZE = 900 * 1024
00063 
00064 
00065 def GetEmail(prompt):
00066   """Prompts the user for their email address and returns it.
00067 
00068   The last used email address is saved to a file and offered up as a suggestion
00069   to the user. If the user presses enter without typing in anything the last
00070   used email address is used. If the user enters a new address, it is saved
00071   for next time we prompt.
00072 
00073   """
00074   last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
00075   last_email = ""
00076   if os.path.exists(last_email_file_name):
00077     try:
00078       last_email_file = open(last_email_file_name, "r")
00079       last_email = last_email_file.readline().strip("\n")
00080       last_email_file.close()
00081       prompt += " [%s]" % last_email
00082     except IOError, e:
00083       pass
00084   email = raw_input(prompt + ": ").strip()
00085   if email:
00086     try:
00087       last_email_file = open(last_email_file_name, "w")
00088       last_email_file.write(email)
00089       last_email_file.close()
00090     except IOError, e:
00091       pass
00092   else:
00093     email = last_email
00094   return email
00095 
00096 
00097 def StatusUpdate(msg):
00098   """Print a status message to stdout.
00099 
00100   If 'verbosity' is greater than 0, print the message.
00101 
00102   Args:
00103     msg: The string to print.
00104   """
00105   if verbosity > 0:
00106     print msg
00107 
00108 
00109 def ErrorExit(msg):
00110   """Print an error message to stderr and exit."""
00111   print >>sys.stderr, msg
00112   sys.exit(1)
00113 
00114 
00115 class ClientLoginError(urllib2.HTTPError):
00116   """Raised to indicate there was an error authenticating with ClientLogin."""
00117 
00118   def __init__(self, url, code, msg, headers, args):
00119     urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
00120     self.args = args
00121     self.reason = args["Error"]
00122 
00123 
00124 class AbstractRpcServer(object):
00125   """Provides a common interface for a simple RPC server."""
00126 
00127   def __init__(self, host, auth_function, host_override=None, extra_headers={},
00128                save_cookies=False):
00129     """Creates a new HttpRpcServer.
00130 
00131     Args:
00132       host: The host to send requests to.
00133       auth_function: A function that takes no arguments and returns an
00134         (email, password) tuple when called. Will be called if authentication
00135         is required.
00136       host_override: The host header to send to the server (defaults to host).
00137       extra_headers: A dict of extra headers to append to every request.
00138       save_cookies: If True, save the authentication cookies to local disk.
00139         If False, use an in-memory cookiejar instead.  Subclasses must
00140         implement this functionality.  Defaults to False.
00141     """
00142     self.host = host
00143     self.host_override = host_override
00144     self.auth_function = auth_function
00145     self.authenticated = False
00146     self.extra_headers = extra_headers
00147     self.save_cookies = save_cookies
00148     self.opener = self._GetOpener()
00149     if self.host_override:
00150       logging.info("Server: %s; Host: %s", self.host, self.host_override)
00151     else:
00152       logging.info("Server: %s", self.host)
00153 
00154   def _GetOpener(self):
00155     """Returns an OpenerDirector for making HTTP requests.
00156 
00157     Returns:
00158       A urllib2.OpenerDirector object.
00159     """
00160     raise NotImplementedError()
00161 
00162   def _CreateRequest(self, url, data=None):
00163     """Creates a new urllib request."""
00164     logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
00165     req = urllib2.Request(url, data=data)
00166     if self.host_override:
00167       req.add_header("Host", self.host_override)
00168     for key, value in self.extra_headers.iteritems():
00169       req.add_header(key, value)
00170     return req
00171 
00172   def _GetAuthToken(self, email, password):
00173     """Uses ClientLogin to authenticate the user, returning an auth token.
00174 
00175     Args:
00176       email:    The user's email address
00177       password: The user's password
00178 
00179     Raises:
00180       ClientLoginError: If there was an error authenticating with ClientLogin.
00181       HTTPError: If there was some other form of HTTP error.
00182 
00183     Returns:
00184       The authentication token returned by ClientLogin.
00185     """
00186     account_type = "GOOGLE"
00187     if self.host.endswith(".google.com"):
00188       # Needed for use inside Google.
00189       account_type = "HOSTED"
00190     req = self._CreateRequest(
00191         url="https://www.google.com/accounts/ClientLogin",
00192         data=urllib.urlencode({
00193             "Email": email,
00194             "Passwd": password,
00195             "service": "ah",
00196             "source": "rietveld-codereview-upload",
00197             "accountType": account_type,
00198         }),
00199     )
00200     try:
00201       response = self.opener.open(req)
00202       response_body = response.read()
00203       response_dict = dict(x.split("=")
00204                            for x in response_body.split("\n") if x)
00205       return response_dict["Auth"]
00206     except urllib2.HTTPError, e:
00207       if e.code == 403:
00208         body = e.read()
00209         response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
00210         raise ClientLoginError(req.get_full_url(), e.code, e.msg,
00211                                e.headers, response_dict)
00212       else:
00213         raise
00214 
00215   def _GetAuthCookie(self, auth_token):
00216     """Fetches authentication cookies for an authentication token.
00217 
00218     Args:
00219       auth_token: The authentication token returned by ClientLogin.
00220 
00221     Raises:
00222       HTTPError: If there was an error fetching the authentication cookies.
00223     """
00224     # This is a dummy value to allow us to identify when we're successful.
00225     continue_location = "http://localhost/"
00226     args = {"continue": continue_location, "auth": auth_token}
00227     req = self._CreateRequest("http://%s/_ah/login?%s" %
00228                               (self.host, urllib.urlencode(args)))
00229     try:
00230       response = self.opener.open(req)
00231     except urllib2.HTTPError, e:
00232       response = e
00233     if (response.code != 302 or
00234         response.info()["location"] != continue_location):
00235       raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
00236                               response.headers, response.fp)
00237     self.authenticated = True
00238 
00239   def _Authenticate(self):
00240     """Authenticates the user.
00241 
00242     The authentication process works as follows:
00243      1) We get a username and password from the user
00244      2) We use ClientLogin to obtain an AUTH token for the user
00245         (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
00246      3) We pass the auth token to /_ah/login on the server to obtain an
00247         authentication cookie. If login was successful, it tries to redirect
00248         us to the URL we provided.
00249 
00250     If we attempt to access the upload API without first obtaining an
00251     authentication cookie, it returns a 401 response and directs us to
00252     authenticate ourselves with ClientLogin.
00253     """
00254     for i in range(3):
00255       credentials = self.auth_function()
00256       try:
00257         auth_token = self._GetAuthToken(credentials[0], credentials[1])
00258       except ClientLoginError, e:
00259         if e.reason == "BadAuthentication":
00260           print >>sys.stderr, "Invalid username or password."
00261           continue
00262         if e.reason == "CaptchaRequired":
00263           print >>sys.stderr, (
00264               "Please go to\n"
00265               "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
00266               "and verify you are a human.  Then try again.")
00267           break
00268         if e.reason == "NotVerified":
00269           print >>sys.stderr, "Account not verified."
00270           break
00271         if e.reason == "TermsNotAgreed":
00272           print >>sys.stderr, "User has not agreed to TOS."
00273           break
00274         if e.reason == "AccountDeleted":
00275           print >>sys.stderr, "The user account has been deleted."
00276           break
00277         if e.reason == "AccountDisabled":
00278           print >>sys.stderr, "The user account has been disabled."
00279           break
00280         if e.reason == "ServiceDisabled":
00281           print >>sys.stderr, ("The user's access to the service has been "
00282                                "disabled.")
00283           break
00284         if e.reason == "ServiceUnavailable":
00285           print >>sys.stderr, "The service is not available; try again later."
00286           break
00287         raise
00288       self._GetAuthCookie(auth_token)
00289       return
00290 
00291   def Send(self, request_path, payload=None,
00292            content_type="application/octet-stream",
00293            timeout=None,
00294            **kwargs):
00295     """Sends an RPC and returns the response.
00296 
00297     Args:
00298       request_path: The path to send the request to, eg /api/appversion/create.
00299       payload: The body of the request, or None to send an empty request.
00300       content_type: The Content-Type header to use.
00301       timeout: timeout in seconds; default None i.e. no timeout.
00302         (Note: for large requests on OS X, the timeout doesn't work right.)
00303       kwargs: Any keyword arguments are converted into query string parameters.
00304 
00305     Returns:
00306       The response body, as a string.
00307     """
00308     # TODO: Don't require authentication.  Let the server say
00309     # whether it is necessary.
00310     if not self.authenticated:
00311       self._Authenticate()
00312 
00313     old_timeout = socket.getdefaulttimeout()
00314     socket.setdefaulttimeout(timeout)
00315     try:
00316       tries = 0
00317       while True:
00318         tries += 1
00319         args = dict(kwargs)
00320         url = "http://%s%s" % (self.host, request_path)
00321         if args:
00322           url += "?" + urllib.urlencode(args)
00323         req = self._CreateRequest(url=url, data=payload)
00324         req.add_header("Content-Type", content_type)
00325         try:
00326           f = self.opener.open(req)
00327           response = f.read()
00328           f.close()
00329           return response
00330         except urllib2.HTTPError, e:
00331           if tries > 3:
00332             raise
00333           elif e.code == 401:
00334             self._Authenticate()
00335 ##           elif e.code >= 500 and e.code < 600:
00336 ##             # Server Error - try again.
00337 ##             continue
00338           else:
00339             raise
00340     finally:
00341       socket.setdefaulttimeout(old_timeout)
00342 
00343 
00344 class HttpRpcServer(AbstractRpcServer):
00345   """Provides a simplified RPC-style interface for HTTP requests."""
00346 
00347   def _Authenticate(self):
00348     """Save the cookie jar after authentication."""
00349     super(HttpRpcServer, self)._Authenticate()
00350     if self.save_cookies:
00351       StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
00352       self.cookie_jar.save()
00353 
00354   def _GetOpener(self):
00355     """Returns an OpenerDirector that supports cookies and ignores redirects.
00356 
00357     Returns:
00358       A urllib2.OpenerDirector object.
00359     """
00360     opener = urllib2.OpenerDirector()
00361     opener.add_handler(urllib2.ProxyHandler())
00362     opener.add_handler(urllib2.UnknownHandler())
00363     opener.add_handler(urllib2.HTTPHandler())
00364     opener.add_handler(urllib2.HTTPDefaultErrorHandler())
00365     opener.add_handler(urllib2.HTTPSHandler())
00366     opener.add_handler(urllib2.HTTPErrorProcessor())
00367     if self.save_cookies:
00368       self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")
00369       self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
00370       if os.path.exists(self.cookie_file):
00371         try:
00372           self.cookie_jar.load()
00373           self.authenticated = True
00374           StatusUpdate("Loaded authentication cookies from %s" %
00375                        self.cookie_file)
00376         except (cookielib.LoadError, IOError):
00377           # Failed to load cookies - just ignore them.
00378           pass
00379       else:
00380         # Create an empty cookie file with mode 600
00381         fd = os.open(self.cookie_file, os.O_CREAT, 0600)
00382         os.close(fd)
00383       # Always chmod the cookie file
00384       os.chmod(self.cookie_file, 0600)
00385     else:
00386       # Don't save cookies across runs of update.py.
00387       self.cookie_jar = cookielib.CookieJar()
00388     opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
00389     return opener
00390 
00391 
00392 parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")
00393 parser.add_option("-y", "--assume_yes", action="store_true",
00394                   dest="assume_yes", default=False,
00395                   help="Assume that the answer to yes/no questions is 'yes'.")
00396 # Logging
00397 group = parser.add_option_group("Logging options")
00398 group.add_option("-q", "--quiet", action="store_const", const=0,
00399                  dest="verbose", help="Print errors only.")
00400 group.add_option("-v", "--verbose", action="store_const", const=2,
00401                  dest="verbose", default=1,
00402                  help="Print info level logs (default).")
00403 group.add_option("--noisy", action="store_const", const=3,
00404                  dest="verbose", help="Print all logs.")
00405 # Review server
00406 group = parser.add_option_group("Review server options")
00407 group.add_option("-s", "--server", action="store", dest="server",
00408                  default="codereview.appspot.com",
00409                  metavar="SERVER",
00410                  help=("The server to upload to. The format is host[:port]. "
00411                        "Defaults to 'codereview.appspot.com'."))
00412 group.add_option("-e", "--email", action="store", dest="email",
00413                  metavar="EMAIL", default=None,
00414                  help="The username to use. Will prompt if omitted.")
00415 group.add_option("-H", "--host", action="store", dest="host",
00416                  metavar="HOST", default=None,
00417                  help="Overrides the Host header sent with all RPCs.")
00418 group.add_option("--no_cookies", action="store_false",
00419                  dest="save_cookies", default=True,
00420                  help="Do not save authentication cookies to local disk.")
00421 # Issue
00422 group = parser.add_option_group("Issue options")
00423 group.add_option("-d", "--description", action="store", dest="description",
00424                  metavar="DESCRIPTION", default=None,
00425                  help="Optional description when creating an issue.")
00426 group.add_option("-f", "--description_file", action="store",
00427                  dest="description_file", metavar="DESCRIPTION_FILE",
00428                  default=None,
00429                  help="Optional path of a file that contains "
00430                       "the description when creating an issue.")
00431 group.add_option("-r", "--reviewers", action="store", dest="reviewers",
00432                  metavar="REVIEWERS", default=None,
00433                  help="Add reviewers (comma separated email addresses).")
00434 group.add_option("--cc", action="store", dest="cc",
00435                  metavar="CC", default=None,
00436                  help="Add CC (comma separated email addresses).")
00437 # Upload options
00438 group = parser.add_option_group("Patch options")
00439 group.add_option("-m", "--message", action="store", dest="message",
00440                  metavar="MESSAGE", default=None,
00441                  help="A message to identify the patch. "
00442                       "Will prompt if omitted.")
00443 group.add_option("-i", "--issue", type="int", action="store",
00444                  metavar="ISSUE", default=None,
00445                  help="Issue number to which to add. Defaults to new issue.")
00446 group.add_option("--download_base", action="store_true",
00447                  dest="download_base", default=False,
00448                  help="Base files will be downloaded by the server "
00449                  "(side-by-side diffs may not work on files with CRs).")
00450 group.add_option("--rev", action="store", dest="revision",
00451                  metavar="REV", default=None,
00452                  help="Branch/tree/revision to diff against (used by DVCS).")
00453 group.add_option("--send_mail", action="store_true",
00454                  dest="send_mail", default=False,
00455                  help="Send notification email to reviewers.")
00456 
00457 
00458 def GetRpcServer(options):
00459   """Returns an instance of an AbstractRpcServer.
00460 
00461   Returns:
00462     A new AbstractRpcServer, on which RPC calls can be made.
00463   """
00464 
00465   rpc_server_class = HttpRpcServer
00466 
00467   def GetUserCredentials():
00468     """Prompts the user for a username and password."""
00469     email = options.email
00470     if email is None:
00471       email = GetEmail("Email (login for uploading to %s)" % options.server)
00472     password = getpass.getpass("Password for %s: " % email)
00473     return (email, password)
00474 
00475   # If this is the dev_appserver, use fake authentication.
00476   host = (options.host or options.server).lower()
00477   if host == "localhost" or host.startswith("localhost:"):
00478     email = options.email
00479     if email is None:
00480       email = "test@example.com"
00481       logging.info("Using debug user %s.  Override with --email" % email)
00482     server = rpc_server_class(
00483         options.server,
00484         lambda: (email, "password"),
00485         host_override=options.host,
00486         extra_headers={"Cookie":
00487                        'dev_appserver_login="%s:False"' % email},
00488         save_cookies=options.save_cookies)
00489     # Don't try to talk to ClientLogin.
00490     server.authenticated = True
00491     return server
00492 
00493   return rpc_server_class(options.server, GetUserCredentials,
00494                           host_override=options.host,
00495                           save_cookies=options.save_cookies)
00496 
00497 
00498 def EncodeMultipartFormData(fields, files):
00499   """Encode form fields for multipart/form-data.
00500 
00501   Args:
00502     fields: A sequence of (name, value) elements for regular form fields.
00503     files: A sequence of (name, filename, value) elements for data to be
00504            uploaded as files.
00505   Returns:
00506     (content_type, body) ready for httplib.HTTP instance.
00507 
00508   Source:
00509     http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
00510   """
00511   BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
00512   CRLF = '\r\n'
00513   lines = []
00514   for (key, value) in fields:
00515     lines.append('--' + BOUNDARY)
00516     lines.append('Content-Disposition: form-data; name="%s"' % key)
00517     lines.append('')
00518     lines.append(value)
00519   for (key, filename, value) in files:
00520     lines.append('--' + BOUNDARY)
00521     lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
00522              (key, filename))
00523     lines.append('Content-Type: %s' % GetContentType(filename))
00524     lines.append('')
00525     lines.append(value)
00526   lines.append('--' + BOUNDARY + '--')
00527   lines.append('')
00528   body = CRLF.join(lines)
00529   content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
00530   return content_type, body
00531 
00532 
00533 def GetContentType(filename):
00534   """Helper to guess the content-type from the filename."""
00535   return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
00536 
00537 
00538 # Use a shell for subcommands on Windows to get a PATH search.
00539 use_shell = sys.platform.startswith("win")
00540 
00541 def RunShellWithReturnCode(command, print_output=False,
00542                            universal_newlines=True):
00543   """Executes a command and returns the output from stdout and the return code.
00544 
00545   Args:
00546     command: Command to execute.
00547     print_output: If True, the output is printed to stdout.
00548                   If False, both stdout and stderr are ignored.
00549     universal_newlines: Use universal_newlines flag (default: True).
00550 
00551   Returns:
00552     Tuple (output, return code)
00553   """
00554   logging.info("Running %s", command)
00555   p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
00556                        shell=use_shell, universal_newlines=universal_newlines)
00557   if print_output:
00558     output_array = []
00559     while True:
00560       line = p.stdout.readline()
00561       if not line:
00562         break
00563       print line.strip("\n")
00564       output_array.append(line)
00565     output = "".join(output_array)
00566   else:
00567     output = p.stdout.read()
00568   p.wait()
00569   errout = p.stderr.read()
00570   if print_output and errout:
00571     print >>sys.stderr, errout
00572   p.stdout.close()
00573   p.stderr.close()
00574   return output, p.returncode
00575 
00576 
00577 def RunShell(command, silent_ok=False, universal_newlines=True,
00578              print_output=False):
00579   data, retcode = RunShellWithReturnCode(command, print_output,
00580                                          universal_newlines)
00581   if retcode:
00582     ErrorExit("Got error status from %s:\n%s" % (command, data))
00583   if not silent_ok and not data:
00584     ErrorExit("No output from %s" % command)
00585   return data
00586 
00587 
00588 class VersionControlSystem(object):
00589   """Abstract base class providing an interface to the VCS."""
00590 
00591   def __init__(self, options):
00592     """Constructor.
00593 
00594     Args:
00595       options: Command line options.
00596     """
00597     self.options = options
00598 
00599   def GenerateDiff(self, args):
00600     """Return the current diff as a string.
00601 
00602     Args:
00603       args: Extra arguments to pass to the diff command.
00604     """
00605     raise NotImplementedError(
00606         "abstract method -- subclass %s must override" % self.__class__)
00607 
00608   def GetUnknownFiles(self):
00609     """Return a list of files unknown to the VCS."""
00610     raise NotImplementedError(
00611         "abstract method -- subclass %s must override" % self.__class__)
00612 
00613   def CheckForUnknownFiles(self):
00614     """Show an "are you sure?" prompt if there are unknown files."""
00615     unknown_files = self.GetUnknownFiles()
00616     if unknown_files:
00617       print "The following files are not added to version control:"
00618       for line in unknown_files:
00619         print line
00620       prompt = "Are you sure to continue?(y/N) "
00621       answer = raw_input(prompt).strip()
00622       if answer != "y":
00623         ErrorExit("User aborted")
00624 
00625   def GetBaseFile(self, filename):
00626     """Get the content of the upstream version of a file.
00627 
00628     Returns:
00629       A tuple (base_content, new_content, is_binary, status)
00630         base_content: The contents of the base file.
00631         new_content: For text files, this is empty.  For binary files, this is
00632           the contents of the new file, since the diff output won't contain
00633           information to reconstruct the current file.
00634         is_binary: True iff the file is binary.
00635         status: The status of the file.
00636     """
00637 
00638     raise NotImplementedError(
00639         "abstract method -- subclass %s must override" % self.__class__)
00640 
00641 
00642   def GetBaseFiles(self, diff):
00643     """Helper that calls GetBase file for each file in the patch.
00644 
00645     Returns:
00646       A dictionary that maps from filename to GetBaseFile's tuple.  Filenames
00647       are retrieved based on lines that start with "Index:" or
00648       "Property changes on:".
00649     """
00650     files = {}
00651     for line in diff.splitlines(True):
00652       if line.startswith('Index:') or line.startswith('Property changes on:'):
00653         unused, filename = line.split(':', 1)
00654         # On Windows if a file has property changes its filename uses '\'
00655         # instead of '/'.
00656         filename = filename.strip().replace('\\', '/')
00657         files[filename] = self.GetBaseFile(filename)
00658     return files
00659 
00660 
00661   def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
00662                       files):
00663     """Uploads the base files (and if necessary, the current ones as well)."""
00664 
00665     def UploadFile(filename, file_id, content, is_binary, status, is_base):
00666       """Uploads a file to the server."""
00667       file_too_large = False
00668       if is_base:
00669         type = "base"
00670       else:
00671         type = "current"
00672       if len(content) > MAX_UPLOAD_SIZE:
00673         print ("Not uploading the %s file for %s because it's too large." %
00674                (type, filename))
00675         file_too_large = True
00676         content = ""
00677       checksum = md5.new(content).hexdigest()
00678       if options.verbose > 0 and not file_too_large:
00679         print "Uploading %s file for %s" % (type, filename)
00680       url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
00681       form_fields = [("filename", filename),
00682                      ("status", status),
00683                      ("checksum", checksum),
00684                      ("is_binary", str(is_binary)),
00685                      ("is_current", str(not is_base)),
00686                     ]
00687       if file_too_large:
00688         form_fields.append(("file_too_large", "1"))
00689       if options.email:
00690         form_fields.append(("user", options.email))
00691       ctype, body = EncodeMultipartFormData(form_fields,
00692                                             [("data", filename, content)])
00693       response_body = rpc_server.Send(url, body,
00694                                       content_type=ctype)
00695       if not response_body.startswith("OK"):
00696         StatusUpdate("  --> %s" % response_body)
00697         sys.exit(1)
00698 
00699     patches = dict()
00700     [patches.setdefault(v, k) for k, v in patch_list]
00701     for filename in patches.keys():
00702       base_content, new_content, is_binary, status = files[filename]
00703       file_id_str = patches.get(filename)
00704       if file_id_str.find("nobase") != -1:
00705         base_content = None
00706         file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
00707       file_id = int(file_id_str)
00708       if base_content != None:
00709         UploadFile(filename, file_id, base_content, is_binary, status, True)
00710       if new_content != None:
00711         UploadFile(filename, file_id, new_content, is_binary, status, False)
00712 
00713   def IsImage(self, filename):
00714     """Returns true if the filename has an image extension."""
00715     mimetype =  mimetypes.guess_type(filename)[0]
00716     if not mimetype:
00717       return False
00718     return mimetype.startswith("image/")
00719 
00720 
00721 class SubversionVCS(VersionControlSystem):
00722   """Implementation of the VersionControlSystem interface for Subversion."""
00723 
00724   def __init__(self, options):
00725     super(SubversionVCS, self).__init__(options)
00726     if self.options.revision:
00727       match = re.match(r"(\d+)(:(\d+))?", self.options.revision)
00728       if not match:
00729         ErrorExit("Invalid Subversion revision %s." % self.options.revision)
00730       self.rev_start = match.group(1)
00731       self.rev_end = match.group(3)
00732     else:
00733       self.rev_start = self.rev_end = None
00734     # Cache output from "svn list -r REVNO dirname".
00735     # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
00736     self.svnls_cache = {}
00737     # SVN base URL is required to fetch files deleted in an older revision.
00738     # Result is cached to not guess it over and over again in GetBaseFile().
00739     required = self.options.download_base or self.options.revision is not None
00740     self.svn_base = self._GuessBase(required)
00741 
00742   def GuessBase(self, required):
00743     """Wrapper for _GuessBase."""
00744     return self.svn_base
00745 
00746   def _GuessBase(self, required):
00747     """Returns the SVN base URL.
00748 
00749     Args:
00750       required: If true, exits if the url can't be guessed, otherwise None is
00751         returned.
00752     """
00753     info = RunShell(["svn", "info"])
00754     for line in info.splitlines():
00755       words = line.split()
00756       if len(words) == 2 and words[0] == "URL:":
00757         url = words[1]
00758         scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
00759         username, netloc = urllib.splituser(netloc)
00760         if username:
00761           logging.info("Removed username from base URL")
00762         if netloc.endswith("svn.python.org"):
00763           if netloc == "svn.python.org":
00764             if path.startswith("/projects/"):
00765               path = path[9:]
00766           elif netloc != "pythondev@svn.python.org":
00767             ErrorExit("Unrecognized Python URL: %s" % url)
00768           base = "http://svn.python.org/view/*checkout*%s/" % path
00769           logging.info("Guessed Python base = %s", base)
00770         elif netloc.endswith("svn.collab.net"):
00771           if path.startswith("/repos/"):
00772             path = path[6:]
00773           base = "http://svn.collab.net/viewvc/*checkout*%s/" % path
00774           logging.info("Guessed CollabNet base = %s", base)
00775         elif netloc.endswith(".googlecode.com"):
00776           path = path + "/"
00777           base = urlparse.urlunparse(("http", netloc, path, params,
00778                                       query, fragment))
00779           logging.info("Guessed Google Code base = %s", base)
00780         else:
00781           path = path + "/"
00782           base = urlparse.urlunparse((scheme, netloc, path, params,
00783                                       query, fragment))
00784           logging.info("Guessed base = %s", base)
00785         return base
00786     if required:
00787       ErrorExit("Can't find URL in output from svn info")
00788     return None
00789 
00790   def GenerateDiff(self, args):
00791     cmd = ["svn", "diff"]
00792     if self.options.revision:
00793       cmd += ["-r", self.options.revision]
00794     cmd.extend(args)
00795     data = RunShell(cmd)
00796     count = 0
00797     for line in data.splitlines():
00798       if line.startswith("Index:") or line.startswith("Property changes on:"):
00799         count += 1
00800         logging.info(line)
00801     if not count:
00802       ErrorExit("No valid patches found in output from svn diff")
00803     return data
00804 
00805   def _CollapseKeywords(self, content, keyword_str):
00806     """Collapses SVN keywords."""
00807     # svn cat translates keywords but svn diff doesn't. As a result of this
00808     # behavior patching.PatchChunks() fails with a chunk mismatch error.
00809     # This part was originally written by the Review Board development team
00810     # who had the same problem (http://reviews.review-board.org/r/276/).
00811     # Mapping of keywords to known aliases
00812     svn_keywords = {
00813       # Standard keywords
00814       'Date':                ['Date', 'LastChangedDate'],
00815       'Revision':            ['Revision', 'LastChangedRevision', 'Rev'],
00816       'Author':              ['Author', 'LastChangedBy'],
00817       'HeadURL':             ['HeadURL', 'URL'],
00818       'Id':                  ['Id'],
00819 
00820       # Aliases
00821       'LastChangedDate':     ['LastChangedDate', 'Date'],
00822       'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
00823       'LastChangedBy':       ['LastChangedBy', 'Author'],
00824       'URL':                 ['URL', 'HeadURL'],
00825     }
00826 
00827     def repl(m):
00828        if m.group(2):
00829          return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
00830        return "$%s$" % m.group(1)
00831     keywords = [keyword
00832                 for name in keyword_str.split(" ")
00833                 for keyword in svn_keywords.get(name, [])]
00834     return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords), repl, content)
00835 
00836   def GetUnknownFiles(self):
00837     status = RunShell(["svn", "status", "--ignore-externals"], silent_ok=True)
00838     unknown_files = []
00839     for line in status.split("\n"):
00840       if line and line[0] == "?":
00841         unknown_files.append(line)
00842     return unknown_files
00843 
00844   def ReadFile(self, filename):
00845     """Returns the contents of a file."""
00846     file = open(filename, 'rb')
00847     result = ""
00848     try:
00849       result = file.read()
00850     finally:
00851       file.close()
00852     return result
00853 
00854   def GetStatus(self, filename):
00855     """Returns the status of a file."""
00856     if not self.options.revision:
00857       status = RunShell(["svn", "status", "--ignore-externals", filename])
00858       if not status:
00859         ErrorExit("svn status returned no output for %s" % filename)
00860       status_lines = status.splitlines()
00861       # If file is in a cl, the output will begin with
00862       # "\n--- Changelist 'cl_name':\n".  See
00863       # http://svn.collab.net/repos/svn/trunk/notes/changelist-design.txt
00864       if (len(status_lines) == 3 and
00865           not status_lines[0] and
00866           status_lines[1].startswith("--- Changelist")):
00867         status = status_lines[2]
00868       else:
00869         status = status_lines[0]
00870     # If we have a revision to diff against we need to run "svn list"
00871     # for the old and the new revision and compare the results to get
00872     # the correct status for a file.
00873     else:
00874       dirname, relfilename = os.path.split(filename)
00875       if dirname not in self.svnls_cache:
00876         cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
00877         out, returncode = RunShellWithReturnCode(cmd)
00878         if returncode:
00879           ErrorExit("Failed to get status for %s." % filename)
00880         old_files = out.splitlines()
00881         args = ["svn", "list"]
00882         if self.rev_end:
00883           args += ["-r", self.rev_end]
00884         cmd = args + [dirname or "."]
00885         out, returncode = RunShellWithReturnCode(cmd)
00886         if returncode:
00887           ErrorExit("Failed to run command %s" % cmd)
00888         self.svnls_cache[dirname] = (old_files, out.splitlines())
00889       old_files, new_files = self.svnls_cache[dirname]
00890       if relfilename in old_files and relfilename not in new_files:
00891         status = "D   "
00892       elif relfilename in old_files and relfilename in new_files:
00893         status = "M   "
00894       else:
00895         status = "A   "
00896     return status
00897 
00898   def GetBaseFile(self, filename):
00899     status = self.GetStatus(filename)
00900     base_content = None
00901     new_content = None
00902 
00903     # If a file is copied its status will be "A  +", which signifies
00904     # "addition-with-history".  See "svn st" for more information.  We need to
00905     # upload the original file or else diff parsing will fail if the file was
00906     # edited.
00907     if status[0] == "A" and status[3] != "+":
00908       # We'll need to upload the new content if we're adding a binary file
00909       # since diff's output won't contain it.
00910       mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
00911                           silent_ok=True)
00912       base_content = ""
00913       is_binary = mimetype and not mimetype.startswith("text/")
00914       if is_binary and self.IsImage(filename):
00915         new_content = self.ReadFile(filename)
00916     elif (status[0] in ("M", "D", "R") or
00917           (status[0] == "A" and status[3] == "+") or  # Copied file.
00918           (status[0] == " " and status[1] == "M")):  # Property change.
00919       args = []
00920       if self.options.revision:
00921         url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
00922       else:
00923         # Don't change filename, it's needed later.
00924         url = filename
00925         args += ["-r", "BASE"]
00926       cmd = ["svn"] + args + ["propget", "svn:mime-type", url]
00927       mimetype, returncode = RunShellWithReturnCode(cmd)
00928       if returncode:
00929         # File does not exist in the requested revision.
00930         # Reset mimetype, it contains an error message.
00931         mimetype = ""
00932       get_base = False
00933       is_binary = mimetype and not mimetype.startswith("text/")
00934       if status[0] == " ":
00935         # Empty base content just to force an upload.
00936         base_content = ""
00937       elif is_binary:
00938         if self.IsImage(filename):
00939           get_base = True
00940           if status[0] == "M":
00941             if not self.rev_end:
00942               new_content = self.ReadFile(filename)
00943             else:
00944               url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
00945               new_content = RunShell(["svn", "cat", url],
00946                                      universal_newlines=True, silent_ok=True)
00947         else:
00948           base_content = ""
00949       else:
00950         get_base = True
00951 
00952       if get_base:
00953         if is_binary:
00954           universal_newlines = False
00955         else:
00956           universal_newlines = True
00957         if self.rev_start:
00958           # "svn cat -r REV delete_file.txt" doesn't work. cat requires
00959           # the full URL with "@REV" appended instead of using "-r" option.
00960           url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
00961           base_content = RunShell(["svn", "cat", url],
00962                                   universal_newlines=universal_newlines,
00963                                   silent_ok=True)
00964         else:
00965           base_content = RunShell(["svn", "cat", filename],
00966                                   universal_newlines=universal_newlines,
00967                                   silent_ok=True)
00968         if not is_binary:
00969           args = []
00970           if self.rev_start:
00971             url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
00972           else:
00973             url = filename
00974             args += ["-r", "BASE"]
00975           cmd = ["svn"] + args + ["propget", "svn:keywords", url]
00976           keywords, returncode = RunShellWithReturnCode(cmd)
00977           if keywords and not returncode:
00978             base_content = self._CollapseKeywords(base_content, keywords)
00979     else:
00980       StatusUpdate("svn status returned unexpected output: %s" % status)
00981       sys.exit(1)
00982     return base_content, new_content, is_binary, status[0:5]
00983 
00984 
00985 class GitVCS(VersionControlSystem):
00986   """Implementation of the VersionControlSystem interface for Git."""
00987 
00988   def __init__(self, options):
00989     super(GitVCS, self).__init__(options)
00990     # Map of filename -> hash of base file.
00991     self.base_hashes = {}
00992 
00993   def GenerateDiff(self, extra_args):
00994     # This is more complicated than svn's GenerateDiff because we must convert
00995     # the diff output to include an svn-style "Index:" line as well as record
00996     # the hashes of the base files, so we can upload them along with our diff.
00997     if self.options.revision:
00998       extra_args = [self.options.revision] + extra_args
00999     gitdiff = RunShell(["git", "diff", "--full-index"] + extra_args)
01000     svndiff = []
01001     filecount = 0
01002     filename = None
01003     for line in gitdiff.splitlines():
01004       match = re.match(r"diff --git a/(.*) b/.*$", line)
01005       if match:
01006         filecount += 1
01007         filename = match.group(1)
01008         svndiff.append("Index: %s\n" % filename)
01009       else:
01010         # The "index" line in a git diff looks like this (long hashes elided):
01011         #   index 82c0d44..b2cee3f 100755
01012         # We want to save the left hash, as that identifies the base file.
01013         match = re.match(r"index (\w+)\.\.", line)
01014         if match:
01015           self.base_hashes[filename] = match.group(1)
01016       svndiff.append(line + "\n")
01017     if not filecount:
01018       ErrorExit("No valid patches found in output from git diff")
01019     return "".join(svndiff)
01020 
01021   def GetUnknownFiles(self):
01022     status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
01023                       silent_ok=True)
01024     return status.splitlines()
01025 
01026   def GetBaseFile(self, filename):
01027     hash = self.base_hashes[filename]
01028     base_content = None
01029     new_content = None
01030     is_binary = False
01031     if hash == "0" * 40:  # All-zero hash indicates no base file.
01032       status = "A"
01033       base_content = ""
01034     else:
01035       status = "M"
01036       base_content, returncode = RunShellWithReturnCode(["git", "show", hash])
01037       if returncode:
01038         ErrorExit("Got error status from 'git show %s'" % hash)
01039     return (base_content, new_content, is_binary, status)
01040 
01041 
01042 class MercurialVCS(VersionControlSystem):
01043   """Implementation of the VersionControlSystem interface for Mercurial."""
01044 
01045   def __init__(self, options, repo_dir):
01046     super(MercurialVCS, self).__init__(options)
01047     # Absolute path to repository (we can be in a subdir)
01048     self.repo_dir = os.path.normpath(repo_dir)
01049     # Compute the subdir
01050     cwd = os.path.normpath(os.getcwd())
01051     assert cwd.startswith(self.repo_dir)
01052     self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
01053     if self.options.revision:
01054       self.base_rev = self.options.revision
01055     else:
01056       self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
01057 
01058   def _GetRelPath(self, filename):
01059     """Get relative path of a file according to the current directory,
01060     given its logical path in the repo."""
01061     assert filename.startswith(self.subdir), filename
01062     return filename[len(self.subdir):].lstrip(r"\/")
01063 
01064   def GenerateDiff(self, extra_args):
01065     # If no file specified, restrict to the current subdir
01066     extra_args = extra_args or ["."]
01067     cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
01068     data = RunShell(cmd, silent_ok=True)
01069     svndiff = []
01070     filecount = 0
01071     for line in data.splitlines():
01072       m = re.match("diff --git a/(\S+) b/(\S+)", line)
01073       if m:
01074         # Modify line to make it look like as it comes from svn diff.
01075         # With this modification no changes on the server side are required
01076         # to make upload.py work with Mercurial repos.
01077         # NOTE: for proper handling of moved/copied files, we have to use
01078         # the second filename.
01079         filename = m.group(2)
01080         svndiff.append("Index: %s" % filename)
01081         svndiff.append("=" * 67)
01082         filecount += 1
01083         logging.info(line)
01084       else:
01085         svndiff.append(line)
01086     if not filecount:
01087       ErrorExit("No valid patches found in output from hg diff")
01088     return "\n".join(svndiff) + "\n"
01089 
01090   def GetUnknownFiles(self):
01091     """Return a list of files unknown to the VCS."""
01092     args = []
01093     status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
01094         silent_ok=True)
01095     unknown_files = []
01096     for line in status.splitlines():
01097       st, fn = line.split(" ", 1)
01098       if st == "?":
01099         unknown_files.append(fn)
01100     return unknown_files
01101 
01102   def GetBaseFile(self, filename):
01103     # "hg status" and "hg cat" both take a path relative to the current subdir
01104     # rather than to the repo root, but "hg diff" has given us the full path
01105     # to the repo root.
01106     base_content = ""
01107     new_content = None
01108     is_binary = False
01109     oldrelpath = relpath = self._GetRelPath(filename)
01110     # "hg status -C" returns two lines for moved/copied files, one otherwise
01111     out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
01112     out = out.splitlines()
01113     # HACK: strip error message about missing file/directory if it isn't in
01114     # the working copy
01115     if out[0].startswith('%s: ' % relpath):
01116       out = out[1:]
01117     if len(out) > 1:
01118       # Moved/copied => considered as modified, use old filename to
01119       # retrieve base contents
01120       oldrelpath = out[1].strip()
01121       status = "M"
01122     else:
01123       status, _ = out[0].split(' ', 1)
01124     if status != "A":
01125       base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
01126         silent_ok=True)
01127       is_binary = "\0" in base_content  # Mercurial's heuristic
01128     if status != "R":
01129       new_content = open(relpath, "rb").read()
01130       is_binary = is_binary or "\0" in new_content
01131     if is_binary and base_content:
01132       # Fetch again without converting newlines
01133       base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
01134         silent_ok=True, universal_newlines=False)
01135     if not is_binary or not self.IsImage(relpath):
01136       new_content = None
01137     return base_content, new_content, is_binary, status
01138 
01139 
01140 # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
01141 def SplitPatch(data):
01142   """Splits a patch into separate pieces for each file.
01143 
01144   Args:
01145     data: A string containing the output of svn diff.
01146 
01147   Returns:
01148     A list of 2-tuple (filename, text) where text is the svn diff output
01149       pertaining to filename.
01150   """
01151   patches = []
01152   filename = None
01153   diff = []
01154   for line in data.splitlines(True):
01155     new_filename = None
01156     if line.startswith('Index:'):
01157       unused, new_filename = line.split(':', 1)
01158       new_filename = new_filename.strip()
01159     elif line.startswith('Property changes on:'):
01160       unused, temp_filename = line.split(':', 1)
01161       # When a file is modified, paths use '/' between directories, however
01162       # when a property is modified '\' is used on Windows.  Make them the same
01163       # otherwise the file shows up twice.
01164       temp_filename = temp_filename.strip().replace('\\', '/')
01165       if temp_filename != filename:
01166         # File has property changes but no modifications, create a new diff.
01167         new_filename = temp_filename
01168     if new_filename:
01169       if filename and diff:
01170         patches.append((filename, ''.join(diff)))
01171       filename = new_filename
01172       diff = [line]
01173       continue
01174     if diff is not None:
01175       diff.append(line)
01176   if filename and diff:
01177     patches.append((filename, ''.join(diff)))
01178   return patches
01179 
01180 
01181 def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
01182   """Uploads a separate patch for each file in the diff output.
01183 
01184   Returns a list of [patch_key, filename] for each file.
01185   """
01186   patches = SplitPatch(data)
01187   rv = []
01188   for patch in patches:
01189     if len(patch[1]) > MAX_UPLOAD_SIZE:
01190       print ("Not uploading the patch for " + patch[0] +
01191              " because the file is too large.")
01192       continue
01193     form_fields = [("filename", patch[0])]
01194     if not options.download_base:
01195       form_fields.append(("content_upload", "1"))
01196     files = [("data", "data.diff", patch[1])]
01197     ctype, body = EncodeMultipartFormData(form_fields, files)
01198     url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
01199     print "Uploading patch for " + patch[0]
01200     response_body = rpc_server.Send(url, body, content_type=ctype)
01201     lines = response_body.splitlines()
01202     if not lines or lines[0] != "OK":
01203       StatusUpdate("  --> %s" % response_body)
01204       sys.exit(1)
01205     rv.append([lines[1], patch[0]])
01206   return rv
01207 
01208 
01209 def GuessVCS(options):
01210   """Helper to guess the version control system.
01211 
01212   This examines the current directory, guesses which VersionControlSystem
01213   we're using, and returns an instance of the appropriate class.  Exit with an
01214   error if we can't figure it out.
01215 
01216   Returns:
01217     A VersionControlSystem instance. Exits if the VCS can't be guessed.
01218   """
01219   # Mercurial has a command to get the base directory of a repository
01220   # Try running it, but don't die if we don't have hg installed.
01221   # NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
01222   try:
01223     out, returncode = RunShellWithReturnCode(["hg", "root"])
01224     if returncode == 0:
01225       return MercurialVCS(options, out.strip())
01226   except OSError, (errno, message):
01227     if errno != 2:  # ENOENT -- they don't have hg installed.
01228       raise
01229 
01230   # Subversion has a .svn in all working directories.
01231   if os.path.isdir('.svn'):
01232     logging.info("Guessed VCS = Subversion")
01233     return SubversionVCS(options)
01234 
01235   # Git has a command to test if you're in a git tree.
01236   # Try running it, but don't die if we don't have git installed.
01237   try:
01238     out, returncode = RunShellWithReturnCode(["git", "rev-parse",
01239                                               "--is-inside-work-tree"])
01240     if returncode == 0:
01241       return GitVCS(options)
01242   except OSError, (errno, message):
01243     if errno != 2:  # ENOENT -- they don't have git installed.
01244       raise
01245 
01246   ErrorExit(("Could not guess version control system. "
01247              "Are you in a working copy directory?"))
01248 
01249 
01250 def RealMain(argv, data=None):
01251   """The real main function.
01252 
01253   Args:
01254     argv: Command line arguments.
01255     data: Diff contents. If None (default) the diff is generated by
01256       the VersionControlSystem implementation returned by GuessVCS().
01257 
01258   Returns:
01259     A 2-tuple (issue id, patchset id).
01260     The patchset id is None if the base files are not uploaded by this
01261     script (applies only to SVN checkouts).
01262   """
01263   logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
01264                               "%(lineno)s %(message)s "))
01265   os.environ['LC_ALL'] = 'C'
01266   options, args = parser.parse_args(argv[1:])
01267   global verbosity
01268   verbosity = options.verbose
01269   if verbosity >= 3:
01270     logging.getLogger().setLevel(logging.DEBUG)
01271   elif verbosity >= 2:
01272     logging.getLogger().setLevel(logging.INFO)
01273   vcs = GuessVCS(options)
01274   if isinstance(vcs, SubversionVCS):
01275     # base field is only allowed for Subversion.
01276     # Note: Fetching base files may become deprecated in future releases.
01277     base = vcs.GuessBase(options.download_base)
01278   else:
01279     base = None
01280   if not base and options.download_base:
01281     options.download_base = True
01282     logging.info("Enabled upload of base file")
01283   if not options.assume_yes:
01284     vcs.CheckForUnknownFiles()
01285   if data is None:
01286     data = vcs.GenerateDiff(args)
01287   files = vcs.GetBaseFiles(data)
01288   if verbosity >= 1:
01289     print "Upload server:", options.server, "(change with -s/--server)"
01290   if options.issue:
01291     prompt = "Message describing this patch set: "
01292   else:
01293     prompt = "New issue subject: "
01294   message = options.message or raw_input(prompt).strip()
01295   if not message:
01296     ErrorExit("A non-empty message is required")
01297   rpc_server = GetRpcServer(options)
01298   form_fields = [("subject", message)]
01299   if base:
01300     form_fields.append(("base", base))
01301   if options.issue:
01302     form_fields.append(("issue", str(options.issue)))
01303   if options.email:
01304     form_fields.append(("user", options.email))
01305   if options.reviewers:
01306     for reviewer in options.reviewers.split(','):
01307       if "@" in reviewer and not reviewer.split("@")[1].count(".") == 1:
01308         ErrorExit("Invalid email address: %s" % reviewer)
01309     form_fields.append(("reviewers", options.reviewers))
01310   if options.cc:
01311     for cc in options.cc.split(','):
01312       if "@" in cc and not cc.split("@")[1].count(".") == 1:
01313         ErrorExit("Invalid email address: %s" % cc)
01314     form_fields.append(("cc", options.cc))
01315   description = options.description
01316   if options.description_file:
01317     if options.description:
01318       ErrorExit("Can't specify description and description_file")
01319     file = open(options.description_file, 'r')
01320     description = file.read()
01321     file.close()
01322   if description:
01323     form_fields.append(("description", description))
01324   # Send a hash of all the base file so the server can determine if a copy
01325   # already exists in an earlier patchset.
01326   base_hashes = ""
01327   for file, info in files.iteritems():
01328     if not info[0] is None:
01329       checksum = md5.new(info[0]).hexdigest()
01330       if base_hashes:
01331         base_hashes += "|"
01332       base_hashes += checksum + ":" + file
01333   form_fields.append(("base_hashes", base_hashes))
01334   # If we're uploading base files, don't send the email before the uploads, so
01335   # that it contains the file status.
01336   if options.send_mail and options.download_base:
01337     form_fields.append(("send_mail", "1"))
01338   if not options.download_base:
01339     form_fields.append(("content_upload", "1"))
01340   if len(data) > MAX_UPLOAD_SIZE:
01341     print "Patch is large, so uploading file patches separately."
01342     uploaded_diff_file = []
01343     form_fields.append(("separate_patches", "1"))
01344   else:
01345     uploaded_diff_file = [("data", "data.diff", data)]
01346   ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
01347   response_body = rpc_server.Send("/upload", body, content_type=ctype)
01348   patchset = None
01349   if not options.download_base or not uploaded_diff_file:
01350     lines = response_body.splitlines()
01351     if len(lines) >= 2:
01352       msg = lines[0]
01353       patchset = lines[1].strip()
01354       patches = [x.split(" ", 1) for x in lines[2:]]
01355     else:
01356       msg = response_body
01357   else:
01358     msg = response_body
01359   StatusUpdate(msg)
01360   if not response_body.startswith("Issue created.") and \
01361   not response_body.startswith("Issue updated."):
01362     sys.exit(0)
01363   issue = msg[msg.rfind("/")+1:]
01364 
01365   if not uploaded_diff_file:
01366     result = UploadSeparatePatches(issue, rpc_server, patchset, data, options)
01367     if not options.download_base:
01368       patches = result
01369 
01370   if not options.download_base:
01371     vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
01372     if options.send_mail:
01373       rpc_server.Send("/" + issue + "/mail", payload="")
01374   return issue, patchset
01375 
01376 
01377 def main():
01378   try:
01379     RealMain(sys.argv)
01380   except KeyboardInterrupt:
01381     print
01382     StatusUpdate("Interrupted.")
01383     sys.exit(1)
01384 
01385 
01386 if __name__ == "__main__":
01387   main()


rc_visard_driver
Author(s): Heiko Hirschmueller , Christian Emmerich , Felix Ruess
autogenerated on Thu Jun 6 2019 20:43:07