Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1898)

Unified Diff: third_party/upload.py

Issue 2027008: Update upload.py to r529 (Closed)
Patch Set: Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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))
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698