1448 lines · 51.8 KB · Python
RawDownload
#!/usr/bin/env python## Copyright 2007 Google Inc.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##     http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License. """Tool for uploading diffs from a version control system to the codereview app. Usage summary: upload.py [options] [-- diff_options] Diff options are passed to the diff command of the underlying system. Supported version control systems:  Git  Mercurial  Subversion It is important for Git/Mercurial users to specify a tree/node/branch to diffagainst by using the '--rev' option."""# This code is derived from appcfg.py in the App Engine SDK (open source),# and from ASPN recipe #146306. import cookielibimport getpassimport loggingimport mimetypesimport optparseimport osimport reimport socketimport subprocessimport sysimport urllibimport urllib2import urlparse # The md5 module was deprecated in Python 2.5.try:  from hashlib import md5except ImportError:  from md5 import md5 try:  import readlineexcept ImportError:  pass # The logging verbosity:#  0: Errors only.#  1: Status messages.#  2: Info logs.#  3: Debug logs.verbosity = 1 # Max size of patch or base file.MAX_UPLOAD_SIZE = 900 * 1024 # Constants for version control names.  Used by GuessVCSName.VCS_GIT = "Git"VCS_MERCURIAL = "Mercurial"VCS_SUBVERSION = "Subversion"VCS_UNKNOWN = "Unknown"  def GetEmail(prompt):  """Prompts the user for their email address and returns it.   The last used email address is saved to a file and offered up as a suggestion  to the user. If the user presses enter without typing in anything the last  used email address is used. If the user enters a new address, it is saved  for next time we prompt.   """  last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")  last_email = ""  if os.path.exists(last_email_file_name):    try:      last_email_file = open(last_email_file_name, "r")      last_email = last_email_file.readline().strip("\n")      last_email_file.close()      prompt += " [%s]" % last_email    except IOError, e:      pass  email = raw_input(prompt + ": ").strip()  if email:    try:      last_email_file = open(last_email_file_name, "w")      last_email_file.write(email)      last_email_file.close()    except IOError, e:      pass  else:    email = last_email  return email  def StatusUpdate(msg):  """Print a status message to stdout.   If 'verbosity' is greater than 0, print the message.   Args:    msg: The string to print.  """  if verbosity > 0:    print msg  def ErrorExit(msg):  """Print an error message to stderr and exit."""  print >>sys.stderr, msg  sys.exit(1)  class ClientLoginError(urllib2.HTTPError):  """Raised to indicate there was an error authenticating with ClientLogin."""   def __init__(self, url, code, msg, headers, args):    urllib2.HTTPError.__init__(self, url, code, msg, headers, None)    self.args = args    self.reason = args["Error"]  class AbstractRpcServer(object):  """Provides a common interface for a simple RPC server."""   def __init__(self, host, auth_function, host_override=None, extra_headers={},               save_cookies=False):    """Creates a new HttpRpcServer.     Args:      host: The host to send requests to.      auth_function: A function that takes no arguments and returns an        (email, password) tuple when called. Will be called if authentication        is required.      host_override: The host header to send to the server (defaults to host).      extra_headers: A dict of extra headers to append to every request.      save_cookies: If True, save the authentication cookies to local disk.        If False, use an in-memory cookiejar instead.  Subclasses must        implement this functionality.  Defaults to False.    """    self.host = host    self.host_override = host_override    self.auth_function = auth_function    self.authenticated = False    self.extra_headers = extra_headers    self.save_cookies = save_cookies    self.opener = self._GetOpener()    if self.host_override:      logging.info("Server: %s; Host: %s", self.host, self.host_override)    else:      logging.info("Server: %s", self.host)   def _GetOpener(self):    """Returns an OpenerDirector for making HTTP requests.     Returns:      A urllib2.OpenerDirector object.    """    raise NotImplementedError()   def _CreateRequest(self, url, data=None):    """Creates a new urllib request."""    logging.debug("Creating request for: '%s' with payload:\n%s", url, data)    req = urllib2.Request(url, data=data)    if self.host_override:      req.add_header("Host", self.host_override)    for key, value in self.extra_headers.iteritems():      req.add_header(key, value)    return req   def _GetAuthToken(self, email, password):    """Uses ClientLogin to authenticate the user, returning an auth token.     Args:      email:    The user's email address      password: The user's password     Raises:      ClientLoginError: If there was an error authenticating with ClientLogin.      HTTPError: If there was some other form of HTTP error.     Returns:      The authentication token returned by ClientLogin.    """    account_type = "GOOGLE"    if self.host.endswith(".google.com"):      # Needed for use inside Google.      account_type = "HOSTED"    req = self._CreateRequest(        url="https://www.google.com/accounts/ClientLogin",        data=urllib.urlencode({            "Email": email,            "Passwd": password,            "service": "ah",            "source": "rietveld-codereview-upload",            "accountType": account_type,        }),    )    try:      response = self.opener.open(req)      response_body = response.read()      response_dict = dict(x.split("=")                           for x in response_body.split("\n") if x)      return response_dict["Auth"]    except urllib2.HTTPError, e:      if e.code == 403:        body = e.read()        response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)        raise ClientLoginError(req.get_full_url(), e.code, e.msg,                               e.headers, response_dict)      else:        raise   def _GetAuthCookie(self, auth_token):    """Fetches authentication cookies for an authentication token.     Args:      auth_token: The authentication token returned by ClientLogin.     Raises:      HTTPError: If there was an error fetching the authentication cookies.    """    # This is a dummy value to allow us to identify when we're successful.    continue_location = "http://localhost/"    args = {"continue": continue_location, "auth": auth_token}    req = self._CreateRequest("http://%s/_ah/login?%s" %                              (self.host, urllib.urlencode(args)))    try:      response = self.opener.open(req)    except urllib2.HTTPError, e:      response = e    if (response.code != 302 or        response.info()["location"] != continue_location):      raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,                              response.headers, response.fp)    self.authenticated = True   def _Authenticate(self):    """Authenticates the user.     The authentication process works as follows:     1) We get a username and password from the user     2) We use ClientLogin to obtain an AUTH token for the user        (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).     3) We pass the auth token to /_ah/login on the server to obtain an        authentication cookie. If login was successful, it tries to redirect        us to the URL we provided.     If we attempt to access the upload API without first obtaining an    authentication cookie, it returns a 401 response and directs us to    authenticate ourselves with ClientLogin.    """    for i in range(3):      credentials = self.auth_function()      try:        auth_token = self._GetAuthToken(credentials[0], credentials[1])      except ClientLoginError, e:        if e.reason == "BadAuthentication":          print >>sys.stderr, "Invalid username or password."          continue        if e.reason == "CaptchaRequired":          print >>sys.stderr, (              "Please go to\n"              "https://www.google.com/accounts/DisplayUnlockCaptcha\n"              "and verify you are a human.  Then try again.")          break        if e.reason == "NotVerified":          print >>sys.stderr, "Account not verified."          break        if e.reason == "TermsNotAgreed":          print >>sys.stderr, "User has not agreed to TOS."          break        if e.reason == "AccountDeleted":          print >>sys.stderr, "The user account has been deleted."          break        if e.reason == "AccountDisabled":          print >>sys.stderr, "The user account has been disabled."          break        if e.reason == "ServiceDisabled":          print >>sys.stderr, ("The user's access to the service has been "                               "disabled.")          break        if e.reason == "ServiceUnavailable":          print >>sys.stderr, "The service is not available; try again later."          break        raise      self._GetAuthCookie(auth_token)      return   def Send(self, request_path, payload=None,           content_type="application/octet-stream",           timeout=None,           **kwargs):    """Sends an RPC and returns the response.     Args:      request_path: The path to send the request to, eg /api/appversion/create.      payload: The body of the request, or None to send an empty request.      content_type: The Content-Type header to use.      timeout: timeout in seconds; default None i.e. no timeout.        (Note: for large requests on OS X, the timeout doesn't work right.)      kwargs: Any keyword arguments are converted into query string parameters.     Returns:      The response body, as a string.    """    # TODO: Don't require authentication.  Let the server say    # whether it is necessary.    if not self.authenticated:      self._Authenticate()     old_timeout = socket.getdefaulttimeout()    socket.setdefaulttimeout(timeout)    try:      tries = 0      while True:        tries += 1        args = dict(kwargs)        url = "http://%s%s" % (self.host, request_path)        if args:          url += "?" + urllib.urlencode(args)        req = self._CreateRequest(url=url, data=payload)        req.add_header("Content-Type", content_type)        try:          f = self.opener.open(req)          response = f.read()          f.close()          return response        except urllib2.HTTPError, e:          if tries > 3:            raise          elif e.code == 401:            self._Authenticate()##           elif e.code >= 500 and e.code < 600:##             # Server Error - try again.##             continue          else:            raise    finally:      socket.setdefaulttimeout(old_timeout)  class HttpRpcServer(AbstractRpcServer):  """Provides a simplified RPC-style interface for HTTP requests."""   def _Authenticate(self):    """Save the cookie jar after authentication."""    super(HttpRpcServer, self)._Authenticate()    if self.save_cookies:      StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)      self.cookie_jar.save()   def _GetOpener(self):    """Returns an OpenerDirector that supports cookies and ignores redirects.     Returns:      A urllib2.OpenerDirector object.    """    opener = urllib2.OpenerDirector()    opener.add_handler(urllib2.ProxyHandler())    opener.add_handler(urllib2.UnknownHandler())    opener.add_handler(urllib2.HTTPHandler())    opener.add_handler(urllib2.HTTPDefaultErrorHandler())    opener.add_handler(urllib2.HTTPSHandler())    opener.add_handler(urllib2.HTTPErrorProcessor())    if self.save_cookies:      self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies")      self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)      if os.path.exists(self.cookie_file):        try:          self.cookie_jar.load()          self.authenticated = True          StatusUpdate("Loaded authentication cookies from %s" %                       self.cookie_file)        except (cookielib.LoadError, IOError):          # Failed to load cookies - just ignore them.          pass      else:        # Create an empty cookie file with mode 600        fd = os.open(self.cookie_file, os.O_CREAT, 0600)        os.close(fd)      # Always chmod the cookie file      os.chmod(self.cookie_file, 0600)    else:      # Don't save cookies across runs of update.py.      self.cookie_jar = cookielib.CookieJar()    opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))    return opener  parser = optparse.OptionParser(usage="%prog [options] [-- diff_options]")parser.add_option("-y", "--assume_yes", action="store_true",                  dest="assume_yes", default=False,                  help="Assume that the answer to yes/no questions is 'yes'.")# Logginggroup = parser.add_option_group("Logging options")group.add_option("-q", "--quiet", action="store_const", const=0,                 dest="verbose", help="Print errors only.")group.add_option("-v", "--verbose", action="store_const", const=2,                 dest="verbose", default=1,                 help="Print info level logs (default).")group.add_option("--noisy", action="store_const", const=3,                 dest="verbose", help="Print all logs.")# Review servergroup = parser.add_option_group("Review server options")group.add_option("-s", "--server", action="store", dest="server",                 default="codereview.appspot.com",                 metavar="SERVER",                 help=("The server to upload to. The format is host[:port]. "                       "Defaults to '%default'."))group.add_option("-e", "--email", action="store", dest="email",                 metavar="EMAIL", default=None,                 help="The username to use. Will prompt if omitted.")group.add_option("-H", "--host", action="store", dest="host",                 metavar="HOST", default=None,                 help="Overrides the Host header sent with all RPCs.")group.add_option("--no_cookies", action="store_false",                 dest="save_cookies", default=True,                 help="Do not save authentication cookies to local disk.")# Issuegroup = parser.add_option_group("Issue options")group.add_option("-d", "--description", action="store", dest="description",                 metavar="DESCRIPTION", default=None,                 help="Optional description when creating an issue.")group.add_option("-f", "--description_file", action="store",                 dest="description_file", metavar="DESCRIPTION_FILE",                 default=None,                 help="Optional path of a file that contains "                      "the description when creating an issue.")group.add_option("-r", "--reviewers", action="store", dest="reviewers",                 metavar="REVIEWERS", default=None,                 help="Add reviewers (comma separated email addresses).")group.add_option("--cc", action="store", dest="cc",                 metavar="CC", default=None,                 help="Add CC (comma separated email addresses).")# Upload optionsgroup = parser.add_option_group("Patch options")group.add_option("-m", "--message", action="store", dest="message",                 metavar="MESSAGE", default=None,                 help="A message to identify the patch. "                      "Will prompt if omitted.")group.add_option("-i", "--issue", type="int", action="store",                 metavar="ISSUE", default=None,                 help="Issue number to which to add. Defaults to new issue.")group.add_option("--download_base", action="store_true",                 dest="download_base", default=False,                 help="Base files will be downloaded by the server "                 "(side-by-side diffs may not work on files with CRs).")group.add_option("--rev", action="store", dest="revision",                 metavar="REV", default=None,                 help="Branch/tree/revision to diff against (used by DVCS).")group.add_option("--send_mail", action="store_true",                 dest="send_mail", default=False,                 help="Send notification email to reviewers.")  def GetRpcServer(options):  """Returns an instance of an AbstractRpcServer.   Returns:    A new AbstractRpcServer, on which RPC calls can be made.  """   rpc_server_class = HttpRpcServer   def GetUserCredentials():    """Prompts the user for a username and password."""    email = options.email    if email is None:      email = GetEmail("Email (login for uploading to %s)" % options.server)    password = getpass.getpass("Password for %s: " % email)    return (email, password)   # If this is the dev_appserver, use fake authentication.  host = (options.host or options.server).lower()  if host == "localhost" or host.startswith("localhost:"):    email = options.email    if email is None:      email = "test@example.com"      logging.info("Using debug user %s.  Override with --email" % email)    server = rpc_server_class(        options.server,        lambda: (email, "password"),        host_override=options.host,        extra_headers={"Cookie":                       'dev_appserver_login="%s:False"' % email},        save_cookies=options.save_cookies)    # Don't try to talk to ClientLogin.

Lines 1–500 of 1448.