Fix tests that rely on the ordering of values in a dictionary.
Reviewed in https://codereview.appspot.com/7311096/.
This commit is contained in:
@@ -863,7 +863,7 @@ class Discovery(unittest.TestCase):
|
|||||||
self.assertTrue(hasattr(new_zoo, 'scopedAnimals'))
|
self.assertTrue(hasattr(new_zoo, 'scopedAnimals'))
|
||||||
self.assertTrue(callable(new_zoo.scopedAnimals))
|
self.assertTrue(callable(new_zoo.scopedAnimals))
|
||||||
|
|
||||||
self.assertEqual(zoo._dynamic_attrs, new_zoo._dynamic_attrs)
|
self.assertEqual(sorted(zoo._dynamic_attrs), sorted(new_zoo._dynamic_attrs))
|
||||||
self.assertEqual(zoo._baseUrl, new_zoo._baseUrl)
|
self.assertEqual(zoo._baseUrl, new_zoo._baseUrl)
|
||||||
self.assertEqual(zoo._developerKey, new_zoo._developerKey)
|
self.assertEqual(zoo._developerKey, new_zoo._developerKey)
|
||||||
self.assertEqual(zoo._requestBuilder, new_zoo._requestBuilder)
|
self.assertEqual(zoo._requestBuilder, new_zoo._requestBuilder)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import unittest
|
|||||||
from apiclient import push
|
from apiclient import push
|
||||||
from apiclient import model
|
from apiclient import model
|
||||||
from apiclient import http
|
from apiclient import http
|
||||||
|
from test_discovery import assertUrisEqual
|
||||||
|
|
||||||
|
|
||||||
class ClientTokenGeneratorTest(unittest.TestCase):
|
class ClientTokenGeneratorTest(unittest.TestCase):
|
||||||
@@ -92,12 +93,14 @@ class WebhookChannelTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_creation_no_appengine(self):
|
def test_creation_no_appengine(self):
|
||||||
c = push.WebhookChannel('http://example.org')
|
c = push.WebhookChannel('http://example.org')
|
||||||
self.assertEqual('web_hook?url=http%3A%2F%2Fexample.org&app_engine=false',
|
assertUrisEqual(self,
|
||||||
|
'web_hook?url=http%3A%2F%2Fexample.org&app_engine=false',
|
||||||
c.as_header_value())
|
c.as_header_value())
|
||||||
|
|
||||||
def test_creation_appengine(self):
|
def test_creation_appengine(self):
|
||||||
c = push.WebhookChannel('http://example.org', app_engine=True)
|
c = push.WebhookChannel('http://example.org', app_engine=True)
|
||||||
self.assertEqual('web_hook?url=http%3A%2F%2Fexample.org&app_engine=true',
|
assertUrisEqual(self,
|
||||||
|
'web_hook?url=http%3A%2F%2Fexample.org&app_engine=true',
|
||||||
c.as_header_value())
|
c.as_header_value())
|
||||||
|
|
||||||
|
|
||||||
@@ -146,14 +149,16 @@ class SubscriptionTest(unittest.TestCase):
|
|||||||
c = push.WebhookChannel('http://example.org')
|
c = push.WebhookChannel('http://example.org')
|
||||||
s = push.Subscription.for_channel(c)
|
s = push.Subscription.for_channel(c)
|
||||||
self.assertTrue(s.client_token)
|
self.assertTrue(s.client_token)
|
||||||
self.assertEqual('web_hook?url=http%3A%2F%2Fexample.org&app_engine=false',
|
assertUrisEqual(self,
|
||||||
|
'web_hook?url=http%3A%2F%2Fexample.org&app_engine=false',
|
||||||
s.subscribe)
|
s.subscribe)
|
||||||
|
|
||||||
def test_create_for_channel_client_token(self):
|
def test_create_for_channel_client_token(self):
|
||||||
c = push.WebhookChannel('http://example.org')
|
c = push.WebhookChannel('http://example.org')
|
||||||
s = push.Subscription.for_channel(c, client_token='my_token')
|
s = push.Subscription.for_channel(c, client_token='my_token')
|
||||||
self.assertEqual('my_token', s.client_token)
|
self.assertEqual('my_token', s.client_token)
|
||||||
self.assertEqual('web_hook?url=http%3A%2F%2Fexample.org&app_engine=false',
|
assertUrisEqual(self,
|
||||||
|
'web_hook?url=http%3A%2F%2Fexample.org&app_engine=false',
|
||||||
s.subscribe)
|
s.subscribe)
|
||||||
|
|
||||||
def test_subscribe(self):
|
def test_subscribe(self):
|
||||||
|
|||||||
532
upload-diffs.py
532
upload-diffs.py
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
#
|
#
|
||||||
# Copyright 2007 Google Inc.
|
# Copyright 2007 Google Inc.
|
||||||
#
|
#
|
||||||
@@ -35,6 +36,7 @@ against by using the '--rev' option.
|
|||||||
|
|
||||||
import ConfigParser
|
import ConfigParser
|
||||||
import cookielib
|
import cookielib
|
||||||
|
import errno
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import getpass
|
import getpass
|
||||||
import logging
|
import logging
|
||||||
@@ -93,12 +95,6 @@ VCS_PERFORCE = "Perforce"
|
|||||||
VCS_CVS = "CVS"
|
VCS_CVS = "CVS"
|
||||||
VCS_UNKNOWN = "Unknown"
|
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/json',
|
|
||||||
'application/x-javascript', 'application/xml',
|
|
||||||
'application/x-freemind', 'application/x-sh']
|
|
||||||
|
|
||||||
VCS_ABBREVIATIONS = {
|
VCS_ABBREVIATIONS = {
|
||||||
VCS_MERCURIAL.lower(): VCS_MERCURIAL,
|
VCS_MERCURIAL.lower(): VCS_MERCURIAL,
|
||||||
"hg": VCS_MERCURIAL,
|
"hg": VCS_MERCURIAL,
|
||||||
@@ -169,7 +165,15 @@ class ClientLoginError(urllib2.HTTPError):
|
|||||||
def __init__(self, url, code, msg, headers, args):
|
def __init__(self, url, code, msg, headers, args):
|
||||||
urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
|
urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
|
||||||
self.args = args
|
self.args = args
|
||||||
self.reason = args["Error"]
|
self._reason = args["Error"]
|
||||||
|
self.info = args.get("Info", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reason(self):
|
||||||
|
# reason is a property on python 2.7 but a member variable on <=2.6.
|
||||||
|
# self.args is modified so it cannot be used as-is so save the value in
|
||||||
|
# self._reason.
|
||||||
|
return self._reason
|
||||||
|
|
||||||
|
|
||||||
class AbstractRpcServer(object):
|
class AbstractRpcServer(object):
|
||||||
@@ -177,7 +181,7 @@ class AbstractRpcServer(object):
|
|||||||
|
|
||||||
def __init__(self, host, auth_function, host_override=None, extra_headers={},
|
def __init__(self, host, auth_function, host_override=None, extra_headers={},
|
||||||
save_cookies=False, account_type=AUTH_ACCOUNT_TYPE):
|
save_cookies=False, account_type=AUTH_ACCOUNT_TYPE):
|
||||||
"""Creates a new HttpRpcServer.
|
"""Creates a new AbstractRpcServer.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
host: The host to send requests to.
|
host: The host to send requests to.
|
||||||
@@ -219,7 +223,7 @@ class AbstractRpcServer(object):
|
|||||||
def _CreateRequest(self, url, data=None):
|
def _CreateRequest(self, url, data=None):
|
||||||
"""Creates a new urllib request."""
|
"""Creates a new urllib request."""
|
||||||
logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
|
logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
|
||||||
req = urllib2.Request(url, data=data)
|
req = urllib2.Request(url, data=data, headers={"Accept": "text/plain"})
|
||||||
if self.host_override:
|
if self.host_override:
|
||||||
req.add_header("Host", self.host_override)
|
req.add_header("Host", self.host_override)
|
||||||
for key, value in self.extra_headers.iteritems():
|
for key, value in self.extra_headers.iteritems():
|
||||||
@@ -313,37 +317,42 @@ class AbstractRpcServer(object):
|
|||||||
try:
|
try:
|
||||||
auth_token = self._GetAuthToken(credentials[0], credentials[1])
|
auth_token = self._GetAuthToken(credentials[0], credentials[1])
|
||||||
except ClientLoginError, e:
|
except ClientLoginError, e:
|
||||||
|
print >>sys.stderr, ''
|
||||||
if e.reason == "BadAuthentication":
|
if e.reason == "BadAuthentication":
|
||||||
|
if e.info == "InvalidSecondFactor":
|
||||||
|
print >>sys.stderr, (
|
||||||
|
"Use an application-specific password instead "
|
||||||
|
"of your regular account password.\n"
|
||||||
|
"See http://www.google.com/"
|
||||||
|
"support/accounts/bin/answer.py?answer=185833")
|
||||||
|
else:
|
||||||
print >>sys.stderr, "Invalid username or password."
|
print >>sys.stderr, "Invalid username or password."
|
||||||
continue
|
elif e.reason == "CaptchaRequired":
|
||||||
if e.reason == "CaptchaRequired":
|
|
||||||
print >>sys.stderr, (
|
print >>sys.stderr, (
|
||||||
"Please go to\n"
|
"Please go to\n"
|
||||||
"https://www.google.com/accounts/DisplayUnlockCaptcha\n"
|
"https://www.google.com/accounts/DisplayUnlockCaptcha\n"
|
||||||
"and verify you are a human. Then try again.\n"
|
"and verify you are a human. Then try again.\n"
|
||||||
"If you are using a Google Apps account the URL is:\n"
|
"If you are using a Google Apps account the URL is:\n"
|
||||||
"https://www.google.com/a/yourdomain.com/UnlockCaptcha")
|
"https://www.google.com/a/yourdomain.com/UnlockCaptcha")
|
||||||
break
|
elif e.reason == "NotVerified":
|
||||||
if e.reason == "NotVerified":
|
|
||||||
print >>sys.stderr, "Account not verified."
|
print >>sys.stderr, "Account not verified."
|
||||||
break
|
elif e.reason == "TermsNotAgreed":
|
||||||
if e.reason == "TermsNotAgreed":
|
|
||||||
print >>sys.stderr, "User has not agreed to TOS."
|
print >>sys.stderr, "User has not agreed to TOS."
|
||||||
break
|
elif e.reason == "AccountDeleted":
|
||||||
if e.reason == "AccountDeleted":
|
|
||||||
print >>sys.stderr, "The user account has been deleted."
|
print >>sys.stderr, "The user account has been deleted."
|
||||||
break
|
elif e.reason == "AccountDisabled":
|
||||||
if e.reason == "AccountDisabled":
|
|
||||||
print >>sys.stderr, "The user account has been disabled."
|
print >>sys.stderr, "The user account has been disabled."
|
||||||
break
|
break
|
||||||
if e.reason == "ServiceDisabled":
|
elif e.reason == "ServiceDisabled":
|
||||||
print >>sys.stderr, ("The user's access to the service has been "
|
print >>sys.stderr, ("The user's access to the service has been "
|
||||||
"disabled.")
|
"disabled.")
|
||||||
break
|
elif e.reason == "ServiceUnavailable":
|
||||||
if e.reason == "ServiceUnavailable":
|
|
||||||
print >>sys.stderr, "The service is not available; try again later."
|
print >>sys.stderr, "The service is not available; try again later."
|
||||||
break
|
else:
|
||||||
|
# Unknown error.
|
||||||
raise
|
raise
|
||||||
|
print >>sys.stderr, ''
|
||||||
|
continue
|
||||||
self._GetAuthCookie(auth_token)
|
self._GetAuthCookie(auth_token)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -398,14 +407,13 @@ class AbstractRpcServer(object):
|
|||||||
raise
|
raise
|
||||||
elif e.code == 401 or e.code == 302:
|
elif e.code == 401 or e.code == 302:
|
||||||
self._Authenticate()
|
self._Authenticate()
|
||||||
## elif e.code >= 500 and e.code < 600:
|
|
||||||
## # Server Error - try again.
|
|
||||||
## continue
|
|
||||||
elif e.code == 301:
|
elif e.code == 301:
|
||||||
# Handle permanent redirect manually.
|
# Handle permanent redirect manually.
|
||||||
url = e.info()["location"]
|
url = e.info()["location"]
|
||||||
url_loc = urlparse.urlparse(url)
|
url_loc = urlparse.urlparse(url)
|
||||||
self.host = '%s://%s' % (url_loc[0], url_loc[1])
|
self.host = '%s://%s' % (url_loc[0], url_loc[1])
|
||||||
|
elif e.code >= 500:
|
||||||
|
ErrorExit(e.read())
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
@@ -460,8 +468,39 @@ class HttpRpcServer(AbstractRpcServer):
|
|||||||
return opener
|
return opener
|
||||||
|
|
||||||
|
|
||||||
|
class CondensedHelpFormatter(optparse.IndentedHelpFormatter):
|
||||||
|
"""Frees more horizontal space by removing indentation from group
|
||||||
|
options and collapsing arguments between short and long, e.g.
|
||||||
|
'-o ARG, --opt=ARG' to -o --opt ARG"""
|
||||||
|
|
||||||
|
def format_heading(self, heading):
|
||||||
|
return "%s:\n" % heading
|
||||||
|
|
||||||
|
def format_option(self, option):
|
||||||
|
self.dedent()
|
||||||
|
res = optparse.HelpFormatter.format_option(self, option)
|
||||||
|
self.indent()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def format_option_strings(self, option):
|
||||||
|
self.set_long_opt_delimiter(" ")
|
||||||
|
optstr = optparse.HelpFormatter.format_option_strings(self, option)
|
||||||
|
optlist = optstr.split(", ")
|
||||||
|
if len(optlist) > 1:
|
||||||
|
if option.takes_value():
|
||||||
|
# strip METAVAR from all but the last option
|
||||||
|
optlist = [x.split()[0] for x in optlist[:-1]] + optlist[-1:]
|
||||||
|
optstr = " ".join(optlist)
|
||||||
|
return optstr
|
||||||
|
|
||||||
|
|
||||||
parser = optparse.OptionParser(
|
parser = optparse.OptionParser(
|
||||||
usage="%prog [options] [-- diff_options] [path...]")
|
usage="%prog [options] [-- diff_options] [path...]",
|
||||||
|
add_help_option=False,
|
||||||
|
formatter=CondensedHelpFormatter()
|
||||||
|
)
|
||||||
|
parser.add_option("-h", "--help", action="store_true",
|
||||||
|
help="Show this help message and exit.")
|
||||||
parser.add_option("-y", "--assume_yes", action="store_true",
|
parser.add_option("-y", "--assume_yes", action="store_true",
|
||||||
dest="assume_yes", default=False,
|
dest="assume_yes", default=False,
|
||||||
help="Assume that the answer to yes/no questions is 'yes'.")
|
help="Assume that the answer to yes/no questions is 'yes'.")
|
||||||
@@ -500,14 +539,13 @@ group.add_option("--account_type", action="store", dest="account_type",
|
|||||||
"valid choices are 'GOOGLE' and 'HOSTED')."))
|
"valid choices are 'GOOGLE' and 'HOSTED')."))
|
||||||
# Issue
|
# Issue
|
||||||
group = parser.add_option_group("Issue options")
|
group = parser.add_option_group("Issue options")
|
||||||
group.add_option("-d", "--description", action="store", dest="description",
|
group.add_option("-t", "--title", action="store", dest="title",
|
||||||
metavar="DESCRIPTION", default=None,
|
help="New issue subject or new patch set title")
|
||||||
help="Optional description when creating an issue.")
|
group.add_option("-m", "--message", action="store", dest="message",
|
||||||
group.add_option("-f", "--description_file", action="store",
|
|
||||||
dest="description_file", metavar="DESCRIPTION_FILE",
|
|
||||||
default=None,
|
default=None,
|
||||||
help="Optional path of a file that contains "
|
help="New issue description or new patch set message")
|
||||||
"the description when creating an issue.")
|
group.add_option("-F", "--file", action="store", dest="file",
|
||||||
|
default=None, help="Read the message above from file.")
|
||||||
group.add_option("-r", "--reviewers", action="store", dest="reviewers",
|
group.add_option("-r", "--reviewers", action="store", dest="reviewers",
|
||||||
metavar="REVIEWERS", default="jcgregorio@google.com",
|
metavar="REVIEWERS", default="jcgregorio@google.com",
|
||||||
help="Add reviewers (comma separated email addresses).")
|
help="Add reviewers (comma separated email addresses).")
|
||||||
@@ -520,15 +558,11 @@ group.add_option("--private", action="store_true", dest="private",
|
|||||||
help="Make the issue restricted to reviewers and those CCed")
|
help="Make the issue restricted to reviewers and those CCed")
|
||||||
# Upload options
|
# Upload options
|
||||||
group = parser.add_option_group("Patch options")
|
group = 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",
|
group.add_option("-i", "--issue", type="int", action="store",
|
||||||
metavar="ISSUE", default=None,
|
metavar="ISSUE", default=None,
|
||||||
help="Issue number to which to add. Defaults to new issue.")
|
help="Issue number to which to add. Defaults to new issue.")
|
||||||
group.add_option("--base_url", action="store", dest="base_url", default=None,
|
group.add_option("--base_url", action="store", dest="base_url", default=None,
|
||||||
help="Base repository URL (listed as \"Base URL\" when "
|
help="Base URL path for files (listed as \"Base URL\" when "
|
||||||
"viewing issue). If omitted, will be guessed automatically "
|
"viewing issue). If omitted, will be guessed automatically "
|
||||||
"for SVN repos and left blank for others.")
|
"for SVN repos and left blank for others.")
|
||||||
group.add_option("--download_base", action="store_true",
|
group.add_option("--download_base", action="store_true",
|
||||||
@@ -542,6 +576,10 @@ group.add_option("--rev", action="store", dest="revision",
|
|||||||
group.add_option("--send_mail", action="store_true",
|
group.add_option("--send_mail", action="store_true",
|
||||||
dest="send_mail", default=False,
|
dest="send_mail", default=False,
|
||||||
help="Send notification email to reviewers.")
|
help="Send notification email to reviewers.")
|
||||||
|
group.add_option("-p", "--send_patch", action="store_true",
|
||||||
|
dest="send_patch", default=False,
|
||||||
|
help="Same as --send_mail, but include diff as an "
|
||||||
|
"attachment, and prepend email subject with 'PATCH:'.")
|
||||||
group.add_option("--vcs", action="store", dest="vcs",
|
group.add_option("--vcs", action="store", dest="vcs",
|
||||||
metavar="VCS", default=None,
|
metavar="VCS", default=None,
|
||||||
help=("Version control system (optional, usually upload.py "
|
help=("Version control system (optional, usually upload.py "
|
||||||
@@ -549,6 +587,15 @@ group.add_option("--vcs", action="store", dest="vcs",
|
|||||||
group.add_option("--emulate_svn_auto_props", action="store_true",
|
group.add_option("--emulate_svn_auto_props", action="store_true",
|
||||||
dest="emulate_svn_auto_props", default=False,
|
dest="emulate_svn_auto_props", default=False,
|
||||||
help=("Emulate Subversion's auto properties feature."))
|
help=("Emulate Subversion's auto properties feature."))
|
||||||
|
# Git-specific
|
||||||
|
group = parser.add_option_group("Git-specific options")
|
||||||
|
group.add_option("--git_similarity", action="store", dest="git_similarity",
|
||||||
|
metavar="SIM", type="int", default=50,
|
||||||
|
help=("Set the minimum similarity index for detecting renames "
|
||||||
|
"and copies. See `git diff -C`. (default 50)."))
|
||||||
|
group.add_option("--git_no_find_copies", action="store_false", default=True,
|
||||||
|
dest="git_find_copies",
|
||||||
|
help=("Prevents git from looking for copies (default off)."))
|
||||||
# Perforce-specific
|
# Perforce-specific
|
||||||
group = parser.add_option_group("Perforce-specific options "
|
group = parser.add_option_group("Perforce-specific options "
|
||||||
"(overrides P4 environment variables)")
|
"(overrides P4 environment variables)")
|
||||||
@@ -565,6 +612,48 @@ group.add_option("--p4_user", action="store", dest="p4_user",
|
|||||||
metavar="P4_USER", default=None,
|
metavar="P4_USER", default=None,
|
||||||
help=("Perforce user"))
|
help=("Perforce user"))
|
||||||
|
|
||||||
|
|
||||||
|
class KeyringCreds(object):
|
||||||
|
def __init__(self, server, host, email):
|
||||||
|
self.server = server
|
||||||
|
self.host = host
|
||||||
|
self.email = email
|
||||||
|
self.accounts_seen = set()
|
||||||
|
|
||||||
|
def GetUserCredentials(self):
|
||||||
|
"""Prompts the user for a username and password.
|
||||||
|
|
||||||
|
Only use keyring on the initial call. If the keyring contains the wrong
|
||||||
|
password, we want to give the user a chance to enter another one.
|
||||||
|
"""
|
||||||
|
# Create a local alias to the email variable to avoid Python's crazy
|
||||||
|
# scoping rules.
|
||||||
|
global keyring
|
||||||
|
email = self.email
|
||||||
|
if email is None:
|
||||||
|
email = GetEmail("Email (login for uploading to %s)" % self.server)
|
||||||
|
password = None
|
||||||
|
if keyring and not email in self.accounts_seen:
|
||||||
|
try:
|
||||||
|
password = keyring.get_password(self.host, email)
|
||||||
|
except:
|
||||||
|
# Sadly, we have to trap all errors here as
|
||||||
|
# gnomekeyring.IOError inherits from object. :/
|
||||||
|
print "Failed to get password from keyring"
|
||||||
|
keyring = None
|
||||||
|
if password is not None:
|
||||||
|
print "Using password from system keyring."
|
||||||
|
self.accounts_seen.add(email)
|
||||||
|
else:
|
||||||
|
password = getpass.getpass("Password for %s: " % email)
|
||||||
|
if keyring:
|
||||||
|
answer = raw_input("Store password in system keyring?(y/N) ").strip()
|
||||||
|
if answer == "y":
|
||||||
|
keyring.set_password(self.host, email, password)
|
||||||
|
self.accounts_seen.add(email)
|
||||||
|
return (email, password)
|
||||||
|
|
||||||
|
|
||||||
def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
||||||
account_type=AUTH_ACCOUNT_TYPE):
|
account_type=AUTH_ACCOUNT_TYPE):
|
||||||
"""Returns an instance of an AbstractRpcServer.
|
"""Returns an instance of an AbstractRpcServer.
|
||||||
@@ -579,18 +668,16 @@ def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
|||||||
or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE.
|
or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A new AbstractRpcServer, on which RPC calls can be made.
|
A new HttpRpcServer, on which RPC calls can be made.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
rpc_server_class = HttpRpcServer
|
|
||||||
|
|
||||||
# If this is the dev_appserver, use fake authentication.
|
# If this is the dev_appserver, use fake authentication.
|
||||||
host = (host_override or server).lower()
|
host = (host_override or server).lower()
|
||||||
if re.match(r'(http://)?localhost([:/]|$)', host):
|
if re.match(r'(http://)?localhost([:/]|$)', host):
|
||||||
if email is None:
|
if email is None:
|
||||||
email = "test@example.com"
|
email = "test@example.com"
|
||||||
logging.info("Using debug user %s. Override with --email" % email)
|
logging.info("Using debug user %s. Override with --email" % email)
|
||||||
server = rpc_server_class(
|
server = HttpRpcServer(
|
||||||
server,
|
server,
|
||||||
lambda: (email, "password"),
|
lambda: (email, "password"),
|
||||||
host_override=host_override,
|
host_override=host_override,
|
||||||
@@ -602,30 +689,11 @@ def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
|||||||
server.authenticated = True
|
server.authenticated = True
|
||||||
return server
|
return server
|
||||||
|
|
||||||
def GetUserCredentials():
|
return HttpRpcServer(server,
|
||||||
"""Prompts the user for a username and password."""
|
KeyringCreds(server, host, email).GetUserCredentials,
|
||||||
# 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 = None
|
|
||||||
if keyring:
|
|
||||||
password = keyring.get_password(host, local_email)
|
|
||||||
if password is not None:
|
|
||||||
print "Using password from system keyring."
|
|
||||||
else:
|
|
||||||
password = getpass.getpass("Password for %s: " % local_email)
|
|
||||||
if keyring:
|
|
||||||
answer = raw_input("Store password in system keyring?(y/N) ").strip()
|
|
||||||
if answer == "y":
|
|
||||||
keyring.set_password(host, local_email, password)
|
|
||||||
return (local_email, password)
|
|
||||||
|
|
||||||
return rpc_server_class(server,
|
|
||||||
GetUserCredentials,
|
|
||||||
host_override=host_override,
|
host_override=host_override,
|
||||||
save_cookies=save_cookies)
|
save_cookies=save_cookies,
|
||||||
|
account_type=account_type)
|
||||||
|
|
||||||
|
|
||||||
def EncodeMultipartFormData(fields, files):
|
def EncodeMultipartFormData(fields, files):
|
||||||
@@ -675,10 +743,10 @@ def GetContentType(filename):
|
|||||||
# Use a shell for subcommands on Windows to get a PATH search.
|
# Use a shell for subcommands on Windows to get a PATH search.
|
||||||
use_shell = sys.platform.startswith("win")
|
use_shell = sys.platform.startswith("win")
|
||||||
|
|
||||||
def RunShellWithReturnCode(command, print_output=False,
|
def RunShellWithReturnCodeAndStderr(command, print_output=False,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
env=os.environ):
|
env=os.environ):
|
||||||
"""Executes a command and returns the output from stdout and the return code.
|
"""Executes a command and returns the output from stdout, stderr and the return code.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
command: Command to execute.
|
command: Command to execute.
|
||||||
@@ -687,9 +755,11 @@ def RunShellWithReturnCode(command, print_output=False,
|
|||||||
universal_newlines: Use universal_newlines flag (default: True).
|
universal_newlines: Use universal_newlines flag (default: True).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple (output, return code)
|
Tuple (stdout, stderr, return code)
|
||||||
"""
|
"""
|
||||||
logging.info("Running %s", command)
|
logging.info("Running %s", command)
|
||||||
|
env = env.copy()
|
||||||
|
env['LC_MESSAGES'] = 'C'
|
||||||
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
shell=use_shell, universal_newlines=universal_newlines,
|
shell=use_shell, universal_newlines=universal_newlines,
|
||||||
env=env)
|
env=env)
|
||||||
@@ -710,8 +780,15 @@ def RunShellWithReturnCode(command, print_output=False,
|
|||||||
print >>sys.stderr, errout
|
print >>sys.stderr, errout
|
||||||
p.stdout.close()
|
p.stdout.close()
|
||||||
p.stderr.close()
|
p.stderr.close()
|
||||||
return output, p.returncode
|
return output, errout, p.returncode
|
||||||
|
|
||||||
|
def RunShellWithReturnCode(command, print_output=False,
|
||||||
|
universal_newlines=True,
|
||||||
|
env=os.environ):
|
||||||
|
"""Executes a command and returns the output from stdout and the return code."""
|
||||||
|
out, err, retcode = RunShellWithReturnCodeAndStderr(command, print_output,
|
||||||
|
universal_newlines, env)
|
||||||
|
return out, retcode
|
||||||
|
|
||||||
def RunShell(command, silent_ok=False, universal_newlines=True,
|
def RunShell(command, silent_ok=False, universal_newlines=True,
|
||||||
print_output=False, env=os.environ):
|
print_output=False, env=os.environ):
|
||||||
@@ -735,6 +812,12 @@ class VersionControlSystem(object):
|
|||||||
"""
|
"""
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
|
def GetGUID(self):
|
||||||
|
"""Return string to distinguish the repository from others, for example to
|
||||||
|
query all opened review issues for it"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"abstract method -- subclass %s must override" % self.__class__)
|
||||||
|
|
||||||
def PostProcessDiff(self, diff):
|
def PostProcessDiff(self, diff):
|
||||||
"""Return the diff with any special post processing this VCS needs, e.g.
|
"""Return the diff with any special post processing this VCS needs, e.g.
|
||||||
to include an svn-style "Index:"."""
|
to include an svn-style "Index:"."""
|
||||||
@@ -861,15 +944,11 @@ class VersionControlSystem(object):
|
|||||||
return False
|
return False
|
||||||
return mimetype.startswith("image/")
|
return mimetype.startswith("image/")
|
||||||
|
|
||||||
def IsBinary(self, filename):
|
def IsBinaryData(self, data):
|
||||||
"""Returns true if the guessed mimetyped isnt't in text group."""
|
"""Returns true if data contains a null byte."""
|
||||||
mimetype = mimetypes.guess_type(filename)[0]
|
# Derived from how Mercurial's heuristic, see
|
||||||
if not mimetype:
|
# http://selenic.com/hg/file/848a6658069e/mercurial/util.py#l229
|
||||||
return False # e.g. README, "real" binaries usually have an extension
|
return bool(data and "\0" in data)
|
||||||
# special case for text files which don't start with text/
|
|
||||||
if mimetype in TEXT_MIMETYPES:
|
|
||||||
return False
|
|
||||||
return not mimetype.startswith("text/")
|
|
||||||
|
|
||||||
|
|
||||||
class SubversionVCS(VersionControlSystem):
|
class SubversionVCS(VersionControlSystem):
|
||||||
@@ -893,6 +972,9 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
required = self.options.download_base or self.options.revision is not None
|
required = self.options.download_base or self.options.revision is not None
|
||||||
self.svn_base = self._GuessBase(required)
|
self.svn_base = self._GuessBase(required)
|
||||||
|
|
||||||
|
def GetGUID(self):
|
||||||
|
return self._GetInfo("Repository UUID")
|
||||||
|
|
||||||
def GuessBase(self, required):
|
def GuessBase(self, required):
|
||||||
"""Wrapper for _GuessBase."""
|
"""Wrapper for _GuessBase."""
|
||||||
return self.svn_base
|
return self.svn_base
|
||||||
@@ -904,12 +986,11 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
required: If true, exits if the url can't be guessed, otherwise None is
|
required: If true, exits if the url can't be guessed, otherwise None is
|
||||||
returned.
|
returned.
|
||||||
"""
|
"""
|
||||||
info = RunShell(["svn", "info"])
|
url = self._GetInfo("URL")
|
||||||
for line in info.splitlines():
|
if url:
|
||||||
if line.startswith("URL: "):
|
|
||||||
url = line.split()[1]
|
|
||||||
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
|
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
|
||||||
guess = ""
|
guess = ""
|
||||||
|
# TODO(anatoli) - repository specific hacks should be handled by server
|
||||||
if netloc == "svn.python.org" and scheme == "svn+ssh":
|
if netloc == "svn.python.org" and scheme == "svn+ssh":
|
||||||
path = "projects" + path
|
path = "projects" + path
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
@@ -926,6 +1007,18 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
ErrorExit("Can't find URL in output from svn info")
|
ErrorExit("Can't find URL in output from svn info")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _GetInfo(self, key):
|
||||||
|
"""Parses 'svn info' for current dir. Returns value for key or None"""
|
||||||
|
for line in RunShell(["svn", "info"]).splitlines():
|
||||||
|
if line.startswith(key + ": "):
|
||||||
|
return line.split(":", 1)[1].strip()
|
||||||
|
|
||||||
|
def _EscapeFilename(self, filename):
|
||||||
|
"""Escapes filename for SVN commands."""
|
||||||
|
if "@" in filename and not filename.endswith("@"):
|
||||||
|
filename = "%s@" % filename
|
||||||
|
return filename
|
||||||
|
|
||||||
def GenerateDiff(self, args):
|
def GenerateDiff(self, args):
|
||||||
cmd = ["svn", "diff"]
|
cmd = ["svn", "diff"]
|
||||||
if self.options.revision:
|
if self.options.revision:
|
||||||
@@ -993,7 +1086,8 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
def GetStatus(self, filename):
|
def GetStatus(self, filename):
|
||||||
"""Returns the status of a file."""
|
"""Returns the status of a file."""
|
||||||
if not self.options.revision:
|
if not self.options.revision:
|
||||||
status = RunShell(["svn", "status", "--ignore-externals", filename])
|
status = RunShell(["svn", "status", "--ignore-externals",
|
||||||
|
self._EscapeFilename(filename)])
|
||||||
if not status:
|
if not status:
|
||||||
ErrorExit("svn status returned no output for %s" % filename)
|
ErrorExit("svn status returned no output for %s" % filename)
|
||||||
status_lines = status.splitlines()
|
status_lines = status.splitlines()
|
||||||
@@ -1012,15 +1106,22 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
else:
|
else:
|
||||||
dirname, relfilename = os.path.split(filename)
|
dirname, relfilename = os.path.split(filename)
|
||||||
if dirname not in self.svnls_cache:
|
if dirname not in self.svnls_cache:
|
||||||
cmd = ["svn", "list", "-r", self.rev_start, dirname or "."]
|
cmd = ["svn", "list", "-r", self.rev_start,
|
||||||
out, returncode = RunShellWithReturnCode(cmd)
|
self._EscapeFilename(dirname) or "."]
|
||||||
|
out, err, returncode = RunShellWithReturnCodeAndStderr(cmd)
|
||||||
if returncode:
|
if returncode:
|
||||||
ErrorExit("Failed to get status for %s." % filename)
|
# Directory might not yet exist at start revison
|
||||||
|
# svn: Unable to find repository location for 'abc' in revision nnn
|
||||||
|
if re.match('^svn: Unable to find repository location for .+ in revision \d+', err):
|
||||||
|
old_files = ()
|
||||||
|
else:
|
||||||
|
ErrorExit("Failed to get status for %s:\n%s" % (filename, err))
|
||||||
|
else:
|
||||||
old_files = out.splitlines()
|
old_files = out.splitlines()
|
||||||
args = ["svn", "list"]
|
args = ["svn", "list"]
|
||||||
if self.rev_end:
|
if self.rev_end:
|
||||||
args += ["-r", self.rev_end]
|
args += ["-r", self.rev_end]
|
||||||
cmd = args + [dirname or "."]
|
cmd = args + [self._EscapeFilename(dirname) or "."]
|
||||||
out, returncode = RunShellWithReturnCode(cmd)
|
out, returncode = RunShellWithReturnCode(cmd)
|
||||||
if returncode:
|
if returncode:
|
||||||
ErrorExit("Failed to run command %s" % cmd)
|
ErrorExit("Failed to run command %s" % cmd)
|
||||||
@@ -1046,17 +1147,18 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
if status[0] == "A" and status[3] != "+":
|
if status[0] == "A" and status[3] != "+":
|
||||||
# We'll need to upload the new content if we're adding a binary file
|
# We'll need to upload the new content if we're adding a binary file
|
||||||
# since diff's output won't contain it.
|
# since diff's output won't contain it.
|
||||||
mimetype = RunShell(["svn", "propget", "svn:mime-type", filename],
|
mimetype = RunShell(["svn", "propget", "svn:mime-type",
|
||||||
silent_ok=True)
|
self._EscapeFilename(filename)], silent_ok=True)
|
||||||
base_content = ""
|
base_content = ""
|
||||||
is_binary = bool(mimetype) and not mimetype.startswith("text/")
|
is_binary = bool(mimetype) and not mimetype.startswith("text/")
|
||||||
if is_binary and self.IsImage(filename):
|
if is_binary:
|
||||||
new_content = self.ReadFile(filename)
|
new_content = self.ReadFile(filename)
|
||||||
elif (status[0] in ("M", "D", "R") or
|
elif (status[0] in ("M", "D", "R") or
|
||||||
(status[0] == "A" and status[3] == "+") or # Copied file.
|
(status[0] == "A" and status[3] == "+") or # Copied file.
|
||||||
(status[0] == " " and status[1] == "M")): # Property change.
|
(status[0] == " " and status[1] == "M")): # Property change.
|
||||||
args = []
|
args = []
|
||||||
if self.options.revision:
|
if self.options.revision:
|
||||||
|
# filename must not be escaped. We already add an ampersand here.
|
||||||
url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
|
url = "%s/%s@%s" % (self.svn_base, filename, self.rev_start)
|
||||||
else:
|
else:
|
||||||
# Don't change filename, it's needed later.
|
# Don't change filename, it's needed later.
|
||||||
@@ -1071,14 +1173,16 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
else:
|
else:
|
||||||
mimetype = mimetype.strip()
|
mimetype = mimetype.strip()
|
||||||
get_base = False
|
get_base = False
|
||||||
|
# this test for binary is exactly the test prescribed by the
|
||||||
|
# official SVN docs at
|
||||||
|
# http://subversion.apache.org/faq.html#binary-files
|
||||||
is_binary = (bool(mimetype) and
|
is_binary = (bool(mimetype) and
|
||||||
not mimetype.startswith("text/") and
|
not mimetype.startswith("text/") and
|
||||||
not mimetype in TEXT_MIMETYPES)
|
mimetype not in ("image/x-xbitmap", "image/x-xpixmap"))
|
||||||
if status[0] == " ":
|
if status[0] == " ":
|
||||||
# Empty base content just to force an upload.
|
# Empty base content just to force an upload.
|
||||||
base_content = ""
|
base_content = ""
|
||||||
elif is_binary:
|
elif is_binary:
|
||||||
if self.IsImage(filename):
|
|
||||||
get_base = True
|
get_base = True
|
||||||
if status[0] == "M":
|
if status[0] == "M":
|
||||||
if not self.rev_end:
|
if not self.rev_end:
|
||||||
@@ -1087,8 +1191,6 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
|
url = "%s/%s@%s" % (self.svn_base, filename, self.rev_end)
|
||||||
new_content = RunShell(["svn", "cat", url],
|
new_content = RunShell(["svn", "cat", url],
|
||||||
universal_newlines=True, silent_ok=True)
|
universal_newlines=True, silent_ok=True)
|
||||||
else:
|
|
||||||
base_content = ""
|
|
||||||
else:
|
else:
|
||||||
get_base = True
|
get_base = True
|
||||||
|
|
||||||
@@ -1106,7 +1208,8 @@ class SubversionVCS(VersionControlSystem):
|
|||||||
silent_ok=True)
|
silent_ok=True)
|
||||||
else:
|
else:
|
||||||
base_content, ret_code = RunShellWithReturnCode(
|
base_content, ret_code = RunShellWithReturnCode(
|
||||||
["svn", "cat", filename], universal_newlines=universal_newlines)
|
["svn", "cat", self._EscapeFilename(filename)],
|
||||||
|
universal_newlines=universal_newlines)
|
||||||
if ret_code and status[0] == "R":
|
if ret_code and status[0] == "R":
|
||||||
# It's a replaced file without local history (see issue208).
|
# It's a replaced file without local history (see issue208).
|
||||||
# The base file needs to be fetched from the server.
|
# The base file needs to be fetched from the server.
|
||||||
@@ -1144,6 +1247,15 @@ class GitVCS(VersionControlSystem):
|
|||||||
# Map of new filename -> old filename for renames.
|
# Map of new filename -> old filename for renames.
|
||||||
self.renames = {}
|
self.renames = {}
|
||||||
|
|
||||||
|
def GetGUID(self):
|
||||||
|
revlist = RunShell("git rev-list --parents HEAD".split()).splitlines()
|
||||||
|
# M-A: Return the 1st root hash, there could be multiple when a
|
||||||
|
# subtree is merged. In that case, more analysis would need to
|
||||||
|
# be done to figure out which HEAD is the 'most representative'.
|
||||||
|
for r in revlist:
|
||||||
|
if ' ' not in r:
|
||||||
|
return r
|
||||||
|
|
||||||
def PostProcessDiff(self, gitdiff):
|
def PostProcessDiff(self, gitdiff):
|
||||||
"""Converts the diff output to include an svn-style "Index:" line as well
|
"""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
|
as record the hashes of the files, so we can upload them along with our
|
||||||
@@ -1214,9 +1326,33 @@ class GitVCS(VersionControlSystem):
|
|||||||
# this by overriding the environment (but there is still a problem if the
|
# this by overriding the environment (but there is still a problem if the
|
||||||
# git config key "diff.external" is used).
|
# git config key "diff.external" is used).
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
|
if "GIT_EXTERNAL_DIFF" in env:
|
||||||
return RunShell(["git", "diff", "--no-ext-diff", "--full-index", "-M"]
|
del env["GIT_EXTERNAL_DIFF"]
|
||||||
+ extra_args, env=env)
|
# -M/-C will not print the diff for the deleted file when a file is renamed.
|
||||||
|
# This is confusing because the original file will not be shown on the
|
||||||
|
# review when a file is renamed. So, get a diff with ONLY deletes, then
|
||||||
|
# append a diff (with rename detection), without deletes.
|
||||||
|
cmd = [
|
||||||
|
"git", "diff", "--no-color", "--no-ext-diff", "--full-index",
|
||||||
|
"--ignore-submodules",
|
||||||
|
]
|
||||||
|
diff = RunShell(
|
||||||
|
cmd + ["--no-renames", "--diff-filter=D"] + extra_args,
|
||||||
|
env=env, silent_ok=True)
|
||||||
|
if self.options.git_find_copies:
|
||||||
|
similarity_options = ["--find-copies-harder", "-l100000",
|
||||||
|
"-C%s" % self.options.git_similarity ]
|
||||||
|
else:
|
||||||
|
similarity_options = ["-M%s" % self.options.git_similarity ]
|
||||||
|
diff += RunShell(
|
||||||
|
cmd + ["--diff-filter=AMCRT"] + similarity_options + extra_args,
|
||||||
|
env=env, silent_ok=True)
|
||||||
|
|
||||||
|
# The CL could be only file deletion or not. So accept silent diff for both
|
||||||
|
# commands then check for an empty diff manually.
|
||||||
|
if not diff:
|
||||||
|
ErrorExit("No output from %s" % (cmd + extra_args))
|
||||||
|
return diff
|
||||||
|
|
||||||
def GetUnknownFiles(self):
|
def GetUnknownFiles(self):
|
||||||
status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
|
status = RunShell(["git", "ls-files", "--exclude-standard", "--others"],
|
||||||
@@ -1235,14 +1371,14 @@ class GitVCS(VersionControlSystem):
|
|||||||
hash_before, hash_after = self.hashes.get(filename, (None,None))
|
hash_before, hash_after = self.hashes.get(filename, (None,None))
|
||||||
base_content = None
|
base_content = None
|
||||||
new_content = None
|
new_content = None
|
||||||
is_binary = self.IsBinary(filename)
|
|
||||||
status = None
|
status = None
|
||||||
|
|
||||||
if filename in self.renames:
|
if filename in self.renames:
|
||||||
status = "A +" # Match svn attribute name for renames.
|
status = "A +" # Match svn attribute name for renames.
|
||||||
if filename not in self.hashes:
|
if filename not in self.hashes:
|
||||||
# If a rename doesn't change the content, we never get a hash.
|
# If a rename doesn't change the content, we never get a hash.
|
||||||
base_content = RunShell(["git", "show", "HEAD:" + filename])
|
base_content = RunShell(
|
||||||
|
["git", "show", "HEAD:" + filename], silent_ok=True)
|
||||||
elif not hash_before:
|
elif not hash_before:
|
||||||
status = "A"
|
status = "A"
|
||||||
base_content = ""
|
base_content = ""
|
||||||
@@ -1251,11 +1387,10 @@ class GitVCS(VersionControlSystem):
|
|||||||
else:
|
else:
|
||||||
status = "M"
|
status = "M"
|
||||||
|
|
||||||
|
is_binary = self.IsBinaryData(base_content)
|
||||||
is_image = self.IsImage(filename)
|
is_image = self.IsImage(filename)
|
||||||
|
|
||||||
# Grab the before/after content if we need it.
|
# Grab the before/after content if we need it.
|
||||||
# We should include file contents if it's text or it's an image.
|
|
||||||
if not is_binary or is_image:
|
|
||||||
# Grab the base content if we don't have it already.
|
# Grab the base content if we don't have it already.
|
||||||
if base_content is None and hash_before:
|
if base_content is None and hash_before:
|
||||||
base_content = self.GetFileContent(hash_before, is_binary)
|
base_content = self.GetFileContent(hash_before, is_binary)
|
||||||
@@ -1273,6 +1408,10 @@ class CVSVCS(VersionControlSystem):
|
|||||||
def __init__(self, options):
|
def __init__(self, options):
|
||||||
super(CVSVCS, self).__init__(options)
|
super(CVSVCS, self).__init__(options)
|
||||||
|
|
||||||
|
def GetGUID(self):
|
||||||
|
"""For now we don't know how to get repository ID for CVS"""
|
||||||
|
return
|
||||||
|
|
||||||
def GetOriginalContent_(self, filename):
|
def GetOriginalContent_(self, filename):
|
||||||
RunShell(["cvs", "up", filename], silent_ok=True)
|
RunShell(["cvs", "up", filename], silent_ok=True)
|
||||||
# TODO need detect file content encoding
|
# TODO need detect file content encoding
|
||||||
@@ -1282,7 +1421,6 @@ class CVSVCS(VersionControlSystem):
|
|||||||
def GetBaseFile(self, filename):
|
def GetBaseFile(self, filename):
|
||||||
base_content = None
|
base_content = None
|
||||||
new_content = None
|
new_content = None
|
||||||
is_binary = False
|
|
||||||
status = "A"
|
status = "A"
|
||||||
|
|
||||||
output, retcode = RunShellWithReturnCode(["cvs", "status", filename])
|
output, retcode = RunShellWithReturnCode(["cvs", "status", filename])
|
||||||
@@ -1302,7 +1440,7 @@ class CVSVCS(VersionControlSystem):
|
|||||||
status = "D"
|
status = "D"
|
||||||
base_content = self.GetOriginalContent_(filename)
|
base_content = self.GetOriginalContent_(filename)
|
||||||
|
|
||||||
return (base_content, new_content, is_binary, status)
|
return (base_content, new_content, self.IsBinaryData(base_content), status)
|
||||||
|
|
||||||
def GenerateDiff(self, extra_args):
|
def GenerateDiff(self, extra_args):
|
||||||
cmd = ["cvs", "diff", "-u", "-N"]
|
cmd = ["cvs", "diff", "-u", "-N"]
|
||||||
@@ -1312,7 +1450,7 @@ class CVSVCS(VersionControlSystem):
|
|||||||
cmd.extend(extra_args)
|
cmd.extend(extra_args)
|
||||||
data, retcode = RunShellWithReturnCode(cmd)
|
data, retcode = RunShellWithReturnCode(cmd)
|
||||||
count = 0
|
count = 0
|
||||||
if retcode == 0:
|
if retcode in [0, 1]:
|
||||||
for line in data.splitlines():
|
for line in data.splitlines():
|
||||||
if line.startswith("Index:"):
|
if line.startswith("Index:"):
|
||||||
count += 1
|
count += 1
|
||||||
@@ -1324,10 +1462,11 @@ class CVSVCS(VersionControlSystem):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def GetUnknownFiles(self):
|
def GetUnknownFiles(self):
|
||||||
status = RunShell(["cvs", "diff"],
|
data, retcode = RunShellWithReturnCode(["cvs", "diff"])
|
||||||
silent_ok=True)
|
if retcode not in [0, 1]:
|
||||||
|
ErrorExit("Got error status from 'cvs diff':\n%s" % (data,))
|
||||||
unknown_files = []
|
unknown_files = []
|
||||||
for line in status.split("\n"):
|
for line in data.split("\n"):
|
||||||
if line and line[0] == "?":
|
if line and line[0] == "?":
|
||||||
unknown_files.append(line)
|
unknown_files.append(line)
|
||||||
return unknown_files
|
return unknown_files
|
||||||
@@ -1348,11 +1487,17 @@ class MercurialVCS(VersionControlSystem):
|
|||||||
else:
|
else:
|
||||||
self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
|
self.base_rev = RunShell(["hg", "parent", "-q"]).split(':')[1].strip()
|
||||||
|
|
||||||
|
def GetGUID(self):
|
||||||
|
# See chapter "Uniquely identifying a repository"
|
||||||
|
# http://hgbook.red-bean.com/read/customizing-the-output-of-mercurial.html
|
||||||
|
info = RunShell("hg log -r0 --template {node}".split())
|
||||||
|
return info.strip()
|
||||||
|
|
||||||
def _GetRelPath(self, filename):
|
def _GetRelPath(self, filename):
|
||||||
"""Get relative path of a file according to the current directory,
|
"""Get relative path of a file according to the current directory,
|
||||||
given its logical path in the repo."""
|
given its logical path in the repo."""
|
||||||
assert filename.startswith(self.subdir), (filename, self.subdir)
|
absname = os.path.join(self.repo_dir, filename)
|
||||||
return filename[len(self.subdir):].lstrip(r"\/")
|
return os.path.relpath(absname)
|
||||||
|
|
||||||
def GenerateDiff(self, extra_args):
|
def GenerateDiff(self, extra_args):
|
||||||
cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
|
cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
|
||||||
@@ -1391,9 +1536,8 @@ class MercurialVCS(VersionControlSystem):
|
|||||||
return unknown_files
|
return unknown_files
|
||||||
|
|
||||||
def GetBaseFile(self, filename):
|
def GetBaseFile(self, filename):
|
||||||
# "hg status" and "hg cat" both take a path relative to the current subdir
|
# "hg status" and "hg cat" both take a path relative to the current subdir,
|
||||||
# rather than to the repo root, but "hg diff" has given us the full path
|
# but "hg diff" has given us the path relative to the repo root.
|
||||||
# to the repo root.
|
|
||||||
base_content = ""
|
base_content = ""
|
||||||
new_content = None
|
new_content = None
|
||||||
is_binary = False
|
is_binary = False
|
||||||
@@ -1418,15 +1562,15 @@ class MercurialVCS(VersionControlSystem):
|
|||||||
if status != "A":
|
if status != "A":
|
||||||
base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
|
base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
|
||||||
silent_ok=True)
|
silent_ok=True)
|
||||||
is_binary = "\0" in base_content # Mercurial's heuristic
|
is_binary = self.IsBinaryData(base_content)
|
||||||
if status != "R":
|
if status != "R":
|
||||||
new_content = open(relpath, "rb").read()
|
new_content = open(relpath, "rb").read()
|
||||||
is_binary = is_binary or "\0" in new_content
|
is_binary = is_binary or self.IsBinaryData(new_content)
|
||||||
if is_binary and base_content:
|
if is_binary and base_content:
|
||||||
# Fetch again without converting newlines
|
# Fetch again without converting newlines
|
||||||
base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
|
base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
|
||||||
silent_ok=True, universal_newlines=False)
|
silent_ok=True, universal_newlines=False)
|
||||||
if not is_binary or not self.IsImage(relpath):
|
if not is_binary:
|
||||||
new_content = None
|
new_content = None
|
||||||
return base_content, new_content, is_binary, status
|
return base_content, new_content, is_binary, status
|
||||||
|
|
||||||
@@ -1462,15 +1606,19 @@ class PerforceVCS(VersionControlSystem):
|
|||||||
|
|
||||||
ConfirmLogin()
|
ConfirmLogin()
|
||||||
|
|
||||||
if not options.message:
|
if not options.title:
|
||||||
description = self.RunPerforceCommand(["describe", self.p4_changelist],
|
description = self.RunPerforceCommand(["describe", self.p4_changelist],
|
||||||
marshal_output=True)
|
marshal_output=True)
|
||||||
if description and "desc" in description:
|
if description and "desc" in description:
|
||||||
# Rietveld doesn't support multi-line descriptions
|
# Rietveld doesn't support multi-line descriptions
|
||||||
raw_message = description["desc"].strip()
|
raw_title = description["desc"].strip()
|
||||||
lines = raw_message.splitlines()
|
lines = raw_title.splitlines()
|
||||||
if len(lines):
|
if len(lines):
|
||||||
options.message = lines[0]
|
options.title = lines[0]
|
||||||
|
|
||||||
|
def GetGUID(self):
|
||||||
|
"""For now we don't know how to get repository ID for Perforce"""
|
||||||
|
return
|
||||||
|
|
||||||
def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False,
|
def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False,
|
||||||
universal_newlines=True):
|
universal_newlines=True):
|
||||||
@@ -1534,9 +1682,6 @@ class PerforceVCS(VersionControlSystem):
|
|||||||
def IsPendingBinary(self, filename):
|
def IsPendingBinary(self, filename):
|
||||||
return self.IsBinaryHelper(filename, "describe")
|
return self.IsBinaryHelper(filename, "describe")
|
||||||
|
|
||||||
def IsBinary(self, filename):
|
|
||||||
ErrorExit("IsBinary is not safe: call IsBaseBinary or IsPendingBinary")
|
|
||||||
|
|
||||||
def IsBinaryHelper(self, filename, command):
|
def IsBinaryHelper(self, filename, command):
|
||||||
file_types = self.GetFileProperties("type", command)
|
file_types = self.GetFileProperties("type", command)
|
||||||
if not filename in file_types:
|
if not filename in file_types:
|
||||||
@@ -1746,7 +1891,7 @@ class PerforceVCS(VersionControlSystem):
|
|||||||
is_binary = self.IsPendingBinary(filename)
|
is_binary = self.IsPendingBinary(filename)
|
||||||
if status != "D" and status != "SKIP":
|
if status != "D" and status != "SKIP":
|
||||||
relpath = self.GetLocalFilename(filename)
|
relpath = self.GetLocalFilename(filename)
|
||||||
if is_binary and self.IsImage(relpath):
|
if is_binary:
|
||||||
new_content = open(relpath, "rb").read()
|
new_content = open(relpath, "rb").read()
|
||||||
|
|
||||||
return base_content, new_content, is_binary, status
|
return base_content, new_content, is_binary, status
|
||||||
@@ -1839,41 +1984,45 @@ def GuessVCSName(options):
|
|||||||
if attribute.startswith("p4") and value != None:
|
if attribute.startswith("p4") and value != None:
|
||||||
return (VCS_PERFORCE, None)
|
return (VCS_PERFORCE, None)
|
||||||
|
|
||||||
|
def RunDetectCommand(vcs_type, command):
|
||||||
|
"""Helper to detect VCS by executing command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A pair (vcs, output) or None. Throws exception on error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
out, returncode = RunShellWithReturnCode(command)
|
||||||
|
if returncode == 0:
|
||||||
|
return (vcs_type, out.strip())
|
||||||
|
except OSError, (errcode, message):
|
||||||
|
if errcode != errno.ENOENT: # command not found code
|
||||||
|
raise
|
||||||
|
|
||||||
# Mercurial has a command to get the base directory of a repository
|
# Mercurial has a command to get the base directory of a repository
|
||||||
# Try running it, but don't die if we don't have hg installed.
|
# Try running it, but don't die if we don't have hg installed.
|
||||||
# NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
|
# NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
|
||||||
try:
|
res = RunDetectCommand(VCS_MERCURIAL, ["hg", "root"])
|
||||||
out, returncode = RunShellWithReturnCode(["hg", "root"])
|
if res != None:
|
||||||
if returncode == 0:
|
return res
|
||||||
return (VCS_MERCURIAL, out.strip())
|
|
||||||
except OSError, (errno, message):
|
|
||||||
if errno != 2: # ENOENT -- they don't have hg installed.
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Subversion has a .svn in all working directories.
|
# Subversion from 1.7 has a single centralized .svn folder
|
||||||
if os.path.isdir('.svn'):
|
# ( see http://subversion.apache.org/docs/release-notes/1.7.html#wc-ng )
|
||||||
logging.info("Guessed VCS = Subversion")
|
# That's why we use 'svn info' instead of checking for .svn dir
|
||||||
return (VCS_SUBVERSION, None)
|
res = RunDetectCommand(VCS_SUBVERSION, ["svn", "info"])
|
||||||
|
if res != None:
|
||||||
|
return res
|
||||||
|
|
||||||
# Git has a command to test if you're in a git tree.
|
# Git has a command to test if you're in a git tree.
|
||||||
# Try running it, but don't die if we don't have git installed.
|
# Try running it, but don't die if we don't have git installed.
|
||||||
try:
|
res = RunDetectCommand(VCS_GIT, ["git", "rev-parse",
|
||||||
out, returncode = RunShellWithReturnCode(["git", "rev-parse",
|
|
||||||
"--is-inside-work-tree"])
|
"--is-inside-work-tree"])
|
||||||
if returncode == 0:
|
if res != None:
|
||||||
return (VCS_GIT, None)
|
return res
|
||||||
except OSError, (errno, message):
|
|
||||||
if errno != 2: # ENOENT -- they don't have git installed.
|
|
||||||
raise
|
|
||||||
|
|
||||||
# detect CVS repos use `cvs status && $? == 0` rules
|
# detect CVS repos use `cvs status && $? == 0` rules
|
||||||
try:
|
res = RunDetectCommand(VCS_CVS, ["cvs", "status"])
|
||||||
out, returncode = RunShellWithReturnCode(["cvs", "status"])
|
if res != None:
|
||||||
if returncode == 0:
|
return res
|
||||||
return (VCS_CVS, None)
|
|
||||||
except OSError, (errno, message):
|
|
||||||
if errno != 2:
|
|
||||||
raise
|
|
||||||
|
|
||||||
return (VCS_UNKNOWN, None)
|
return (VCS_UNKNOWN, None)
|
||||||
|
|
||||||
@@ -2058,10 +2207,15 @@ def RealMain(argv, data=None):
|
|||||||
The patchset id is None if the base files are not uploaded by this
|
The patchset id is None if the base files are not uploaded by this
|
||||||
script (applies only to SVN checkouts).
|
script (applies only to SVN checkouts).
|
||||||
"""
|
"""
|
||||||
logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
|
|
||||||
"%(lineno)s %(message)s "))
|
|
||||||
os.environ['LC_ALL'] = 'C'
|
|
||||||
options, args = parser.parse_args(argv[1:])
|
options, args = parser.parse_args(argv[1:])
|
||||||
|
if options.help:
|
||||||
|
if options.verbose < 2:
|
||||||
|
# hide Perforce options
|
||||||
|
parser.epilog = "Use '--help -v' to show additional Perforce options."
|
||||||
|
parser.option_groups.remove(parser.get_option_group('--p4_port'))
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
global verbosity
|
global verbosity
|
||||||
verbosity = options.verbose
|
verbosity = options.verbose
|
||||||
if verbosity >= 3:
|
if verbosity >= 3:
|
||||||
@@ -2098,19 +2252,16 @@ def RealMain(argv, data=None):
|
|||||||
files = vcs.GetBaseFiles(data)
|
files = vcs.GetBaseFiles(data)
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
print "Upload server:", options.server, "(change with -s/--server)"
|
print "Upload server:", options.server, "(change with -s/--server)"
|
||||||
if options.issue:
|
|
||||||
prompt = "Message describing this patch set: "
|
|
||||||
else:
|
|
||||||
prompt = "New issue subject: "
|
|
||||||
message = options.message or raw_input(prompt).strip()
|
|
||||||
if not message:
|
|
||||||
ErrorExit("A non-empty message is required")
|
|
||||||
rpc_server = GetRpcServer(options.server,
|
rpc_server = GetRpcServer(options.server,
|
||||||
options.email,
|
options.email,
|
||||||
options.host,
|
options.host,
|
||||||
options.save_cookies,
|
options.save_cookies,
|
||||||
options.account_type)
|
options.account_type)
|
||||||
form_fields = [("subject", message)]
|
form_fields = []
|
||||||
|
|
||||||
|
repo_guid = vcs.GetGUID()
|
||||||
|
if repo_guid:
|
||||||
|
form_fields.append(("repo_guid", repo_guid))
|
||||||
if base:
|
if base:
|
||||||
b = urlparse.urlparse(base)
|
b = urlparse.urlparse(base)
|
||||||
username, netloc = urllib.splituser(b.netloc)
|
username, netloc = urllib.splituser(b.netloc)
|
||||||
@@ -2131,15 +2282,38 @@ def RealMain(argv, data=None):
|
|||||||
for cc in options.cc.split(','):
|
for cc in options.cc.split(','):
|
||||||
CheckReviewer(cc)
|
CheckReviewer(cc)
|
||||||
form_fields.append(("cc", options.cc))
|
form_fields.append(("cc", options.cc))
|
||||||
description = options.description
|
|
||||||
if options.description_file:
|
# Process --message, --title and --file.
|
||||||
if options.description:
|
message = options.message or ""
|
||||||
ErrorExit("Can't specify description and description_file")
|
title = options.title or ""
|
||||||
file = open(options.description_file, 'r')
|
if options.file:
|
||||||
description = file.read()
|
if options.message:
|
||||||
|
ErrorExit("Can't specify both message and message file options")
|
||||||
|
file = open(options.file, 'r')
|
||||||
|
message = file.read()
|
||||||
file.close()
|
file.close()
|
||||||
if description:
|
if options.issue:
|
||||||
form_fields.append(("description", description))
|
prompt = "Title describing this patch set: "
|
||||||
|
else:
|
||||||
|
prompt = "New issue subject: "
|
||||||
|
title = (
|
||||||
|
title or message.split('\n', 1)[0].strip() or raw_input(prompt).strip())
|
||||||
|
if not title and not options.issue:
|
||||||
|
ErrorExit("A non-empty title is required for a new issue")
|
||||||
|
# For existing issues, it's fine to give a patchset an empty name. Rietveld
|
||||||
|
# doesn't accept that so use a whitespace.
|
||||||
|
title = title or " "
|
||||||
|
if len(title) > 100:
|
||||||
|
title = title[:99] + '…'
|
||||||
|
if title and not options.issue:
|
||||||
|
message = message or title
|
||||||
|
|
||||||
|
form_fields.append(("subject", title))
|
||||||
|
# If it's a new issue send message as description. Otherwise a new
|
||||||
|
# message is created below on upload_complete.
|
||||||
|
if message and not options.issue:
|
||||||
|
form_fields.append(("description", message))
|
||||||
|
|
||||||
# Send a hash of all the base file so the server can determine if a copy
|
# Send a hash of all the base file so the server can determine if a copy
|
||||||
# already exists in an earlier patchset.
|
# already exists in an earlier patchset.
|
||||||
base_hashes = ""
|
base_hashes = ""
|
||||||
@@ -2155,10 +2329,8 @@ def RealMain(argv, data=None):
|
|||||||
print "Warning: Private flag ignored when updating an existing issue."
|
print "Warning: Private flag ignored when updating an existing issue."
|
||||||
else:
|
else:
|
||||||
form_fields.append(("private", "1"))
|
form_fields.append(("private", "1"))
|
||||||
# If we're uploading base files, don't send the email before the uploads, so
|
if options.send_patch:
|
||||||
# that it contains the file status.
|
options.send_mail = True
|
||||||
if options.send_mail and options.download_base:
|
|
||||||
form_fields.append(("send_mail", "1"))
|
|
||||||
if not options.download_base:
|
if not options.download_base:
|
||||||
form_fields.append(("content_upload", "1"))
|
form_fields.append(("content_upload", "1"))
|
||||||
if len(data) > MAX_UPLOAD_SIZE:
|
if len(data) > MAX_UPLOAD_SIZE:
|
||||||
@@ -2193,13 +2365,25 @@ def RealMain(argv, data=None):
|
|||||||
|
|
||||||
if not options.download_base:
|
if not options.download_base:
|
||||||
vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
|
vcs.UploadBaseFiles(issue, rpc_server, patches, patchset, options, files)
|
||||||
|
|
||||||
|
payload = {} # payload for final request
|
||||||
if options.send_mail:
|
if options.send_mail:
|
||||||
rpc_server.Send("/" + issue + "/mail", payload="")
|
payload["send_mail"] = "yes"
|
||||||
|
if options.send_patch:
|
||||||
|
payload["attach_patch"] = "yes"
|
||||||
|
if options.issue and message:
|
||||||
|
payload["message"] = message
|
||||||
|
payload = urllib.urlencode(payload)
|
||||||
|
rpc_server.Send("/" + issue + "/upload_complete/" + (patchset or ""),
|
||||||
|
payload=payload)
|
||||||
return issue, patchset
|
return issue, patchset
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
|
logging.basicConfig(format=("%(asctime).19s %(levelname)s %(filename)s:"
|
||||||
|
"%(lineno)s %(message)s "))
|
||||||
|
os.environ['LC_ALL'] = 'C'
|
||||||
RealMain(sys.argv)
|
RealMain(sys.argv)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print
|
print
|
||||||
|
|||||||
Reference in New Issue
Block a user