| Index: third_party/upload.py
|
| diff --git a/third_party/upload.py b/third_party/upload.py
|
| old mode 100755
|
| new mode 100644
|
| index 86403d8fbb5de2b6bdf03342ed8036876967c056..f8e35ea5fce59eead98778ec70566c82acc74d64
|
| --- a/third_party/upload.py
|
| +++ b/third_party/upload.py
|
| @@ -31,7 +31,9 @@ against 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 ConfigParser
|
| import cookielib
|
| +import fnmatch
|
| import getpass
|
| import logging
|
| import mimetypes
|
| @@ -75,7 +77,7 @@ VCS_UNKNOWN = "Unknown"
|
| # whitelist for non-binary filetypes which do not start with "text/"
|
| # .mm (Objective-C) shows up as application/x-freemind on my Linux box.
|
| TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript',
|
| - 'application/x-freemind']
|
| + 'application/xml', 'application/x-freemind']
|
|
|
| VCS_ABBREVIATIONS = {
|
| VCS_MERCURIAL.lower(): VCS_MERCURIAL,
|
| @@ -85,6 +87,8 @@ VCS_ABBREVIATIONS = {
|
| VCS_GIT.lower(): VCS_GIT,
|
| }
|
|
|
| +# The result of parsing Subversion's [auto-props] setting.
|
| +svn_auto_props_map = None
|
|
|
| def GetEmail(prompt):
|
| """Prompts the user for their email address and returns it.
|
| @@ -211,6 +215,9 @@ class AbstractRpcServer(object):
|
| 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({
|
| @@ -229,35 +236,6 @@ class AbstractRpcServer(object):
|
| return response_dict["Auth"]
|
| except urllib2.HTTPError, e:
|
| if e.code == 403:
|
| - # Try a temporary workaround.
|
| - if self.host.endswith(".google.com"):
|
| - 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
|
| -
|
| 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,
|
| @@ -344,6 +322,7 @@ class AbstractRpcServer(object):
|
| def Send(self, request_path, payload=None,
|
| content_type="application/octet-stream",
|
| timeout=None,
|
| + extra_headers=None,
|
| **kwargs):
|
| """Sends an RPC and returns the response.
|
|
|
| @@ -353,6 +332,9 @@ class AbstractRpcServer(object):
|
| 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.)
|
| + extra_headers: Dict containing additional HTTP headers that should be
|
| + included in the request (string header names mapped to their values),
|
| + or None to not include any additional headers.
|
| kwargs: Any keyword arguments are converted into query string parameters.
|
|
|
| Returns:
|
| @@ -375,6 +357,9 @@ class AbstractRpcServer(object):
|
| url += "?" + urllib.urlencode(args)
|
| req = self._CreateRequest(url=url, data=payload)
|
| req.add_header("Content-Type", content_type)
|
| + if extra_headers:
|
| + for header, value in extra_headers.items():
|
| + req.add_header(header, value)
|
| try:
|
| f = self.opener.open(req)
|
| response = f.read()
|
| @@ -499,13 +484,18 @@ group.add_option("-m", "--message", action="store", dest="message",
|
| 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("--base_url", action="store", dest="base_url", default=None,
|
| + help="Base repository URL (listed as \"Base URL\" when "
|
| + "viewing issue). If omitted, will be guessed automatically "
|
| + "for SVN repos and left blank for others.")
|
| 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).")
|
| + help="Base revision/branch/tree to diff against. Use "
|
| + "rev1:rev2 range to review already committed changeset.")
|
| group.add_option("--send_mail", action="store_true",
|
| dest="send_mail", default=False,
|
| help="Send notification email to reviewers.")
|
| @@ -513,46 +503,58 @@ group.add_option("--vcs", action="store", dest="vcs",
|
| metavar="VCS", default=None,
|
| help=("Version control system (optional, usually upload.py "
|
| "already guesses the right VCS)."))
|
| +group.add_option("--emulate_svn_auto_props", action="store_true",
|
| + dest="emulate_svn_auto_props", default=False,
|
| + help=("Emulate Subversion's auto properties feature."))
|
|
|
|
|
| -def GetRpcServer(options):
|
| +def GetRpcServer(server, email=None, host_override=None, save_cookies=True):
|
| """Returns an instance of an AbstractRpcServer.
|
|
|
| + Args:
|
| + server: String containing the review server URL.
|
| + email: String containing user's email address.
|
| + host_override: If not None, string containing an alternate hostname to use
|
| + in the host header.
|
| + save_cookies: Whether authentication cookies should be saved to disk.
|
| +
|
| 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()
|
| + host = (host_override or 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,
|
| + server,
|
| lambda: (email, "password"),
|
| - host_override=options.host,
|
| + host_override=host_override,
|
| extra_headers={"Cookie":
|
| 'dev_appserver_login="%s:False"' % email},
|
| - save_cookies=options.save_cookies)
|
| + save_cookies=save_cookies)
|
| # Don't try to talk to ClientLogin.
|
| server.authenticated = True
|
| return server
|
|
|
| - return rpc_server_class(options.server, GetUserCredentials,
|
| - host_override=options.host,
|
| - save_cookies=options.save_cookies)
|
| + def GetUserCredentials():
|
| + """Prompts the user for a username and password."""
|
| + # Create a local alias to the email variable to avoid Python's crazy
|
| + # scoping rules.
|
| + local_email = email
|
| + if local_email is None:
|
| + local_email = GetEmail("Email (login for uploading to %s)" % server)
|
| + password = getpass.getpass("Password for %s: " % local_email)
|
| + return (local_email, password)
|
| +
|
| + return rpc_server_class(server,
|
| + GetUserCredentials,
|
| + host_override=host_override,
|
| + save_cookies=save_cookies)
|
|
|
|
|
| def EncodeMultipartFormData(fields, files):
|
| @@ -658,6 +660,11 @@ class VersionControlSystem(object):
|
| """
|
| self.options = options
|
|
|
| + def PostProcessDiff(self, diff):
|
| + """Return the diff with any special post processing this VCS needs, e.g.
|
| + to include an svn-style "Index:"."""
|
| + return diff
|
| +
|
| def GenerateDiff(self, args):
|
| """Return the current diff as a string.
|
|
|
| @@ -806,7 +813,7 @@ class SubversionVCS(VersionControlSystem):
|
| # Cache output from "svn list -r REVNO dirname".
|
| # Keys: dirname, Values: 2-tuple (ouput for start rev and end rev).
|
| self.svnls_cache = {}
|
| - # SVN base URL is required to fetch files deleted in an older revision.
|
| + # Base URL is required to fetch files deleted in an older revision.
|
| # Result is cached to not guess it over and over again in GetBaseFile().
|
| required = self.options.download_base or self.options.revision is not None
|
| self.svn_base = self._GuessBase(required)
|
| @@ -1065,31 +1072,38 @@ class GitVCS(VersionControlSystem):
|
| # Map of new filename -> old filename for renames.
|
| self.renames = {}
|
|
|
| - def GenerateDiff(self, extra_args):
|
| - # This is more complicated than svn's GenerateDiff because we must convert
|
| - # the diff output to include an svn-style "Index:" line as well as record
|
| - # the hashes of the files, so we can upload them along with our diff.
|
| -
|
| + def PostProcessDiff(self, gitdiff):
|
| + """Converts the diff output to include an svn-style "Index:" line as well
|
| + as record the hashes of the files, so we can upload them along with our
|
| + diff."""
|
| # Special used by git to indicate "no such content".
|
| NULL_HASH = "0"*40
|
|
|
| - extra_args = extra_args[:]
|
| - if self.options.revision:
|
| - extra_args = [self.options.revision] + extra_args
|
| + def IsFileNew(filename):
|
| + return filename in self.hashes and self.hashes[filename][0] is None
|
| +
|
| + def AddSubversionPropertyChange(filename):
|
| + """Add svn's property change information into the patch if given file is
|
| + new file.
|
| +
|
| + We use Subversion's auto-props setting to retrieve its property.
|
| + See http://svnbook.red-bean.com/en/1.1/ch07.html#svn-ch-7-sect-1.3.2 for
|
| + Subversion's [auto-props] setting.
|
| + """
|
| + if self.options.emulate_svn_auto_props and IsFileNew(filename):
|
| + svnprops = GetSubversionPropertyChanges(filename)
|
| + if svnprops:
|
| + svndiff.append("\n" + svnprops + "\n")
|
|
|
| - # --no-ext-diff is broken in some versions of Git, so try to work around
|
| - # this by overriding the environment (but there is still a problem if the
|
| - # git config key "diff.external" is used).
|
| - env = os.environ.copy()
|
| - if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
|
| - gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"]
|
| - + extra_args, env=env)
|
| svndiff = []
|
| filecount = 0
|
| filename = None
|
| for line in gitdiff.splitlines():
|
| match = re.match(r"diff --git a/(.*) b/(.*)$", line)
|
| if match:
|
| + # Add auto property here for previously seen file.
|
| + if filename is not None:
|
| + AddSubversionPropertyChange(filename)
|
| filecount += 1
|
| # Intentionally use the "after" filename so we can show renames.
|
| filename = match.group(2)
|
| @@ -1111,8 +1125,24 @@ class GitVCS(VersionControlSystem):
|
| svndiff.append(line + "\n")
|
| if not filecount:
|
| ErrorExit("No valid patches found in output from git diff")
|
| + # Add auto property for the last seen file.
|
| + assert filename is not None
|
| + AddSubversionPropertyChange(filename)
|
| return "".join(svndiff)
|
|
|
| + def GenerateDiff(self, extra_args):
|
| + extra_args = extra_args[:]
|
| + if self.options.revision:
|
| + extra_args = [self.options.revision] + extra_args
|
| +
|
| + # --no-ext-diff is broken in some versions of Git, so try to work around
|
| + # this by overriding the environment (but there is still a problem if the
|
| + # git config key "diff.external" is used).
|
| + env = os.environ.copy()
|
| + if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
|
| + return RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"]
|
| + + extra_args, env=env)
|
| +
|
| def GetUnknownFiles(self):
|
| status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
|
| silent_ok=True)
|
| @@ -1137,7 +1167,7 @@ class GitVCS(VersionControlSystem):
|
| status = "A +" # Match svn attribute name for renames.
|
| if filename not in self.hashes:
|
| # If a rename doesn't change the content, we never get a hash.
|
| - base_content = RunShell(["git", "show", filename])
|
| + base_content = RunShell(["git", "show", "HEAD:" + filename])
|
| elif not hash_before:
|
| status = "A"
|
| base_content = ""
|
| @@ -1429,6 +1459,113 @@ def CheckReviewer(reviewer):
|
| ErrorExit("Invalid email address: %r" % reviewer)
|
|
|
|
|
| +def LoadSubversionAutoProperties():
|
| + """Returns the content of [auto-props] section of Subversion's config file as
|
| + a dictionary.
|
| +
|
| + Returns:
|
| + A dictionary whose key-value pair corresponds the [auto-props] section's
|
| + key-value pair.
|
| + In following cases, returns empty dictionary:
|
| + - config file doesn't exist, or
|
| + - 'enable-auto-props' is not set to 'true-like-value' in [miscellany].
|
| + """
|
| + # Todo(hayato): Windows users might use different path for configuration file.
|
| + subversion_config = os.path.expanduser("~/.subversion/config")
|
| + if not os.path.exists(subversion_config):
|
| + return {}
|
| + config = ConfigParser.ConfigParser()
|
| + config.read(subversion_config)
|
| + if (config.has_section("miscellany") and
|
| + config.has_option("miscellany", "enable-auto-props") and
|
| + config.getboolean("miscellany", "enable-auto-props") and
|
| + config.has_section("auto-props")):
|
| + props = {}
|
| + for file_pattern in config.options("auto-props"):
|
| + props[file_pattern] = ParseSubversionPropertyValues(
|
| + config.get("auto-props", file_pattern))
|
| + return props
|
| + else:
|
| + return {}
|
| +
|
| +def ParseSubversionPropertyValues(props):
|
| + """Parse the given property value which comes from [auto-props] section and
|
| + returns a list whose element is a (svn_prop_key, svn_prop_value) pair.
|
| +
|
| + See the following doctest for example.
|
| +
|
| + >>> ParseSubversionPropertyValues('svn:eol-style=LF')
|
| + [('svn:eol-style', 'LF')]
|
| + >>> ParseSubversionPropertyValues('svn:mime-type=image/jpeg')
|
| + [('svn:mime-type', 'image/jpeg')]
|
| + >>> ParseSubversionPropertyValues('svn:eol-style=LF;svn:executable')
|
| + [('svn:eol-style', 'LF'), ('svn:executable', '*')]
|
| + """
|
| + key_value_pairs = []
|
| + for prop in props.split(";"):
|
| + key_value = prop.split("=")
|
| + assert len(key_value) <= 2
|
| + if len(key_value) == 1:
|
| + # If value is not given, use '*' as a Subversion's convention.
|
| + key_value_pairs.append((key_value[0], "*"))
|
| + else:
|
| + key_value_pairs.append((key_value[0], key_value[1]))
|
| + return key_value_pairs
|
| +
|
| +
|
| +def GetSubversionPropertyChanges(filename):
|
| + """Return a Subversion's 'Property changes on ...' string, which is used in
|
| + the patch file.
|
| +
|
| + Args:
|
| + filename: filename whose property might be set by [auto-props] config.
|
| +
|
| + Returns:
|
| + A string like 'Property changes on |filename| ...' if given |filename|
|
| + matches any entries in [auto-props] section. None, otherwise.
|
| + """
|
| + global svn_auto_props_map
|
| + if svn_auto_props_map is None:
|
| + svn_auto_props_map = LoadSubversionAutoProperties()
|
| +
|
| + all_props = []
|
| + for file_pattern, props in svn_auto_props_map.items():
|
| + if fnmatch.fnmatch(filename, file_pattern):
|
| + all_props.extend(props)
|
| + if all_props:
|
| + return FormatSubversionPropertyChanges(filename, all_props)
|
| + return None
|
| +
|
| +
|
| +def FormatSubversionPropertyChanges(filename, props):
|
| + """Returns Subversion's 'Property changes on ...' strings using given filename
|
| + and properties.
|
| +
|
| + Args:
|
| + filename: filename
|
| + props: A list whose element is a (svn_prop_key, svn_prop_value) pair.
|
| +
|
| + Returns:
|
| + A string which can be used in the patch file for Subversion.
|
| +
|
| + See the following doctest for example.
|
| +
|
| + >>> print FormatSubversionPropertyChanges('foo.cc', [('svn:eol-style', 'LF')])
|
| + Property changes on: foo.cc
|
| + ___________________________________________________________________
|
| + Added: svn:eol-style
|
| + + LF
|
| + <BLANKLINE>
|
| + """
|
| + prop_changes_lines = [
|
| + "Property changes on: %s" % filename,
|
| + "___________________________________________________________________"]
|
| + for key, value in props:
|
| + prop_changes_lines.append("Added: " + key)
|
| + prop_changes_lines.append(" + " + value)
|
| + return "\n".join(prop_changes_lines) + "\n"
|
| +
|
| +
|
| def RealMain(argv, data=None):
|
| """The real main function.
|
|
|
| @@ -1452,13 +1589,21 @@ def RealMain(argv, data=None):
|
| logging.getLogger().setLevel(logging.DEBUG)
|
| elif verbosity >= 2:
|
| logging.getLogger().setLevel(logging.INFO)
|
| +
|
| vcs = GuessVCS(options)
|
| +
|
| + base = options.base_url
|
| if isinstance(vcs, SubversionVCS):
|
| - # base field is only allowed for Subversion.
|
| + # Guessing the base field is only supported for Subversion.
|
| # Note: Fetching base files may become deprecated in future releases.
|
| - base = vcs.GuessBase(options.download_base)
|
| - else:
|
| - base = None
|
| + guessed_base = vcs.GuessBase(options.download_base)
|
| + if base:
|
| + if guessed_base and base != guessed_base:
|
| + print "Using base URL \"%s\" from --base_url instead of \"%s\"" % \
|
| + (base, guessed_base)
|
| + else:
|
| + base = guessed_base
|
| +
|
| if not base and options.download_base:
|
| options.download_base = True
|
| logging.info("Enabled upload of base file")
|
| @@ -1466,6 +1611,7 @@ def RealMain(argv, data=None):
|
| vcs.CheckForUnknownFiles()
|
| if data is None:
|
| data = vcs.GenerateDiff(args)
|
| + data = vcs.PostProcessDiff(data)
|
| files = vcs.GetBaseFiles(data)
|
| if verbosity >= 1:
|
| print "Upload server:", options.server, "(change with -s/--server)"
|
| @@ -1476,7 +1622,10 @@ def RealMain(argv, data=None):
|
| message = options.message or raw_input(prompt).strip()
|
| if not message:
|
| ErrorExit("A non-empty message is required")
|
| - rpc_server = GetRpcServer(options)
|
| + rpc_server = GetRpcServer(options.server,
|
| + options.email,
|
| + options.host,
|
| + options.save_cookies)
|
| form_fields = [("subject", message)]
|
| if base:
|
| form_fields.append(("base", base))
|
|
|