From fbe3d3015ca7582252adc7dd966c060e4bb65cd4 Mon Sep 17 00:00:00 2001 From: "maruel@chromium.org" Date: Thu, 24 Mar 2011 17:52:23 +0000 Subject: [PATCH] Update upload.py to r680 from upstream. Removed all the local hacks as they are not necessary anymore. R=dpranke@chromium.org BUG=none TEST=manually verified that HTTP 302 redirect works fine. Review URL: http://codereview.chromium.org/6730004 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@79296 0039d316-1c4b-4281-b951-d872f2087c98 --- git_cl/upload.py | 524 +++++++++++++++++++++++++++++++++++++----- third_party/upload.py | 524 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 940 insertions(+), 108 deletions(-) diff --git a/git_cl/upload.py b/git_cl/upload.py index ff7d3f33e..ee2b4c2b4 100644 --- a/git_cl/upload.py +++ b/git_cl/upload.py @@ -24,6 +24,8 @@ Supported version control systems: Git Mercurial Subversion + Perforce + CVS It is important for Git/Mercurial users to specify a tree/node/branch to diff against by using the '--rev' option. @@ -36,6 +38,7 @@ import cookielib import fnmatch import getpass import logging +import marshal import mimetypes import optparse import os @@ -86,6 +89,8 @@ MAX_UPLOAD_SIZE = 900 * 1024 VCS_GIT = "Git" VCS_MERCURIAL = "Mercurial" VCS_SUBVERSION = "Subversion" +VCS_PERFORCE = "Perforce" +VCS_CVS = "CVS" VCS_UNKNOWN = "Unknown" # whitelist for non-binary filetypes which do not start with "text/" @@ -99,7 +104,10 @@ VCS_ABBREVIATIONS = { "hg": VCS_MERCURIAL, VCS_SUBVERSION.lower(): VCS_SUBVERSION, "svn": VCS_SUBVERSION, + VCS_PERFORCE.lower(): VCS_PERFORCE, + "p4": VCS_PERFORCE, VCS_GIT.lower(): VCS_GIT, + VCS_CVS.lower(): VCS_CVS, } # The result of parsing Subversion's [auto-props] setting. @@ -188,8 +196,6 @@ class AbstractRpcServer(object): if (not self.host.startswith("http://") and not self.host.startswith("https://")): self.host = "http://" + self.host - assert re.match(r'^[a-z]+://[a-z0-9\.-_]+(|:[0-9]+)$', self.host), ( - '%s is malformed' % host) self.host_override = host_override self.auth_function = auth_function self.authenticated = False @@ -220,11 +226,10 @@ class AbstractRpcServer(object): req.add_header(key, value) return req - def _GetAuthToken(self, host, email, password): + def _GetAuthToken(self, email, password): """Uses ClientLogin to authenticate the user, returning an auth token. Args: - host: Host to get a token against. email: The user's email address password: The user's password @@ -236,7 +241,7 @@ class AbstractRpcServer(object): The authentication token returned by ClientLogin. """ account_type = self.account_type - if host.endswith(".google.com"): + if self.host.endswith(".google.com"): # Needed for use inside Google. account_type = "HOSTED" req = self._CreateRequest( @@ -264,12 +269,10 @@ class AbstractRpcServer(object): else: raise - def _GetAuthCookie(self, host, auth_token): + def _GetAuthCookie(self, auth_token): """Fetches authentication cookies for an authentication token. Args: - host: The host to get a cookie against. Because of 301, it may be a - different host than self.host. auth_token: The authentication token returned by ClientLogin. Raises: @@ -278,33 +281,21 @@ class AbstractRpcServer(object): # This is a dummy value to allow us to identify when we're successful. continue_location = "http://localhost/" args = {"continue": continue_location, "auth": auth_token} - tries = 0 - url = "%s/_ah/login?%s" % (host, urllib.urlencode(args)) - while tries < 3: - tries += 1 - req = self._CreateRequest(url) - try: - response = self.opener.open(req) - except urllib2.HTTPError, e: - response = e - if e.code == 301: - # Handle permanent redirect manually. - url = e.info()["location"] - continue - break + req = self._CreateRequest("%s/_ah/login?%s" % + (self.host, urllib.urlencode(args))) + try: + response = self.opener.open(req) + except urllib2.HTTPError, e: + response = e if (response.code != 302 or response.info()["location"] != continue_location): raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg, response.headers, response.fp) self.authenticated = True - def _Authenticate(self, host): + def _Authenticate(self): """Authenticates the user. - Args: - host: The host to get a cookie against. Because of 301, it may be a - different host than self.host. - The authentication process works as follows: 1) We get a username and password from the user 2) We use ClientLogin to obtain an AUTH token for the user @@ -320,7 +311,7 @@ class AbstractRpcServer(object): for i in range(3): credentials = self.auth_function() try: - auth_token = self._GetAuthToken(host, credentials[0], credentials[1]) + auth_token = self._GetAuthToken(credentials[0], credentials[1]) except ClientLoginError, e: if e.reason == "BadAuthentication": print >>sys.stderr, "Invalid username or password." @@ -353,7 +344,7 @@ class AbstractRpcServer(object): print >>sys.stderr, "The service is not available; try again later." break raise - self._GetAuthCookie(host, auth_token) + self._GetAuthCookie(auth_token) return def Send(self, request_path, payload=None, @@ -380,18 +371,18 @@ class AbstractRpcServer(object): # TODO: Don't require authentication. Let the server say # whether it is necessary. if not self.authenticated: - self._Authenticate(self.host) + self._Authenticate() old_timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(timeout) try: tries = 0 - args = dict(kwargs) - url = "%s%s" % (self.host, request_path) - if args: - url += "?" + urllib.urlencode(args) while True: tries += 1 + args = dict(kwargs) + url = "%s%s" % (self.host, request_path) + if args: + url += "?" + urllib.urlencode(args) req = self._CreateRequest(url=url, data=payload) req.add_header("Content-Type", content_type) if extra_headers: @@ -406,24 +397,17 @@ class AbstractRpcServer(object): if tries > 3: raise elif e.code == 401 or e.code == 302: - url_loc = urlparse.urlparse(url) - self._Authenticate('%s://%s' % (url_loc[0], url_loc[1])) + self._Authenticate() ## elif e.code >= 500 and e.code < 600: ## # Server Error - try again. ## continue elif e.code == 301: # Handle permanent redirect manually. url = e.info()["location"] + url_loc = urlparse.urlparse(url) + self.host = '%s://%s' % (url_loc[0], url_loc[1]) else: raise - except urllib2.URLError, e: - reason = getattr(e, 'reason', None) - if isinstance(reason, str) and reason.find("110") != -1: - # Connection timeout error. - if tries <= 3: - # Try again. - continue - raise finally: socket.setdefaulttimeout(old_timeout) @@ -431,9 +415,9 @@ class AbstractRpcServer(object): class HttpRpcServer(AbstractRpcServer): """Provides a simplified RPC-style interface for HTTP requests.""" - def _Authenticate(self, *args): + def _Authenticate(self): """Save the cookie jar after authentication.""" - super(HttpRpcServer, self)._Authenticate(*args) + super(HttpRpcServer, self)._Authenticate() if self.save_cookies: StatusUpdate("Saving authentication cookies to %s" % self.cookie_file) self.cookie_jar.save() @@ -490,6 +474,8 @@ group.add_option("-v", "--verbose", action="store_const", const=2, help="Print info level logs.") group.add_option("--noisy", action="store_const", const=3, dest="verbose", help="Print all logs.") +group.add_option("--print_diffs", dest="print_diffs", action="store_true", + help="Print full diffs.") # Review server group = parser.add_option_group("Review server options") group.add_option("-s", "--server", action="store", dest="server", @@ -562,7 +548,21 @@ group.add_option("--vcs", action="store", dest="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.")) - +# Perforce-specific +group = parser.add_option_group("Perforce-specific options " + "(overrides P4 environment variables)") +group.add_option("--p4_port", action="store", dest="p4_port", + metavar="P4_PORT", default=None, + help=("Perforce server and port (optional)")) +group.add_option("--p4_changelist", action="store", dest="p4_changelist", + metavar="P4_CHANGELIST", default=None, + help=("Perforce changelist id")) +group.add_option("--p4_client", action="store", dest="p4_client", + metavar="P4_CLIENT", default=None, + help=("Perforce client/workspace")) +group.add_option("--p4_user", action="store", dest="p4_user", + metavar="P4_USER", default=None, + help=("Perforce user")) def GetRpcServer(server, email=None, host_override=None, save_cookies=True, account_type=AUTH_ACCOUNT_TYPE): @@ -908,9 +908,6 @@ class SubversionVCS(VersionControlSystem): if line.startswith("URL: "): url = line.split()[1] scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) - username, netloc = urllib.splituser(netloc) - if username: - logging.info("Removed username from base URL") guess = "" if netloc == "svn.python.org" and scheme == "svn+ssh": path = "projects" + path @@ -1070,8 +1067,12 @@ class SubversionVCS(VersionControlSystem): # File does not exist in the requested revision. # Reset mimetype, it contains an error message. mimetype = "" + else: + mimetype = mimetype.strip() get_base = False - is_binary = bool(mimetype) and not mimetype.startswith("text/") + is_binary = (bool(mimetype) and + not mimetype.startswith("text/") and + not mimetype in TEXT_MIMETYPES) if status[0] == " ": # Empty base content just to force an upload. base_content = "" @@ -1265,6 +1266,71 @@ class GitVCS(VersionControlSystem): return (base_content, new_content, is_binary, status) +class CVSVCS(VersionControlSystem): + """Implementation of the VersionControlSystem interface for CVS.""" + + def __init__(self, options): + super(CVSVCS, self).__init__(options) + + def GetOriginalContent_(self, filename): + RunShell(["cvs", "up", filename], silent_ok=True) + # TODO need detect file content encoding + content = open(filename).read() + return content.replace("\r\n", "\n") + + def GetBaseFile(self, filename): + base_content = None + new_content = None + is_binary = False + status = "A" + + output, retcode = RunShellWithReturnCode(["cvs", "status", filename]) + if retcode: + ErrorExit("Got error status from 'cvs status %s'" % filename) + + if output.find("Status: Locally Modified") != -1: + status = "M" + temp_filename = "%s.tmp123" % filename + os.rename(filename, temp_filename) + base_content = self.GetOriginalContent_(filename) + os.rename(temp_filename, filename) + elif output.find("Status: Locally Added"): + status = "A" + base_content = "" + elif output.find("Status: Needs Checkout"): + status = "D" + base_content = self.GetOriginalContent_(filename) + + return (base_content, new_content, is_binary, status) + + def GenerateDiff(self, extra_args): + cmd = ["cvs", "diff", "-u", "-N"] + if self.options.revision: + cmd += ["-r", self.options.revision] + + cmd.extend(extra_args) + data, retcode = RunShellWithReturnCode(cmd) + count = 0 + if retcode == 0: + for line in data.splitlines(): + if line.startswith("Index:"): + count += 1 + logging.info(line) + + if not count: + ErrorExit("No valid patches found in output from cvs diff") + + return data + + def GetUnknownFiles(self): + status = RunShell(["cvs", "diff"], + silent_ok=True) + unknown_files = [] + for line in status.split("\n"): + if line and line[0] == "?": + unknown_files.append(line) + return unknown_files + class MercurialVCS(VersionControlSystem): """Implementation of the VersionControlSystem interface for Mercurial.""" @@ -1364,6 +1430,326 @@ class MercurialVCS(VersionControlSystem): return base_content, new_content, is_binary, status +class PerforceVCS(VersionControlSystem): + """Implementation of the VersionControlSystem interface for Perforce.""" + + def __init__(self, options): + + def ConfirmLogin(): + # Make sure we have a valid perforce session + while True: + data, retcode = self.RunPerforceCommandWithReturnCode( + ["login", "-s"], marshal_output=True) + if not data: + ErrorExit("Error checking perforce login") + if not retcode and (not "code" in data or data["code"] != "error"): + break + print "Enter perforce password: " + self.RunPerforceCommandWithReturnCode(["login"]) + + super(PerforceVCS, self).__init__(options) + + self.p4_changelist = options.p4_changelist + if not self.p4_changelist: + ErrorExit("A changelist id is required") + if (options.revision): + ErrorExit("--rev is not supported for perforce") + + self.p4_port = options.p4_port + self.p4_client = options.p4_client + self.p4_user = options.p4_user + + ConfirmLogin() + + if not options.message: + description = self.RunPerforceCommand(["describe", self.p4_changelist], + marshal_output=True) + if description and "desc" in description: + # Rietveld doesn't support multi-line descriptions + raw_message = description["desc"].strip() + lines = raw_message.splitlines() + if len(lines): + options.message = lines[0] + + def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False, + universal_newlines=True): + args = ["p4"] + if marshal_output: + # -G makes perforce format its output as marshalled python objects + args.extend(["-G"]) + if self.p4_port: + args.extend(["-p", self.p4_port]) + if self.p4_client: + args.extend(["-c", self.p4_client]) + if self.p4_user: + args.extend(["-u", self.p4_user]) + args.extend(extra_args) + + data, retcode = RunShellWithReturnCode( + args, print_output=False, universal_newlines=universal_newlines) + if marshal_output and data: + data = marshal.loads(data) + return data, retcode + + def RunPerforceCommand(self, extra_args, marshal_output=False, + universal_newlines=True): + # This might be a good place to cache call results, since things like + # describe or fstat might get called repeatedly. + data, retcode = self.RunPerforceCommandWithReturnCode( + extra_args, marshal_output, universal_newlines) + if retcode: + ErrorExit("Got error status from %s:\n%s" % (extra_args, data)) + return data + + def GetFileProperties(self, property_key_prefix = "", command = "describe"): + description = self.RunPerforceCommand(["describe", self.p4_changelist], + marshal_output=True) + + changed_files = {} + file_index = 0 + # Try depotFile0, depotFile1, ... until we don't find a match + while True: + file_key = "depotFile%d" % file_index + if file_key in description: + filename = description[file_key] + change_type = description[property_key_prefix + str(file_index)] + changed_files[filename] = change_type + file_index += 1 + else: + break + return changed_files + + def GetChangedFiles(self): + return self.GetFileProperties("action") + + def GetUnknownFiles(self): + # Perforce doesn't detect new files, they have to be explicitly added + return [] + + def IsBaseBinary(self, filename): + base_filename = self.GetBaseFilename(filename) + return self.IsBinaryHelper(base_filename, "files") + + def IsPendingBinary(self, filename): + return self.IsBinaryHelper(filename, "describe") + + def IsBinary(self, filename): + ErrorExit("IsBinary is not safe: call IsBaseBinary or IsPendingBinary") + + def IsBinaryHelper(self, filename, command): + file_types = self.GetFileProperties("type", command) + if not filename in file_types: + ErrorExit("Trying to check binary status of unknown file %s." % filename) + # This treats symlinks, macintosh resource files, temporary objects, and + # unicode as binary. See the Perforce docs for more details: + # http://www.perforce.com/perforce/doc.current/manuals/cmdref/o.ftypes.html + return not file_types[filename].endswith("text") + + def GetFileContent(self, filename, revision, is_binary): + file_arg = filename + if revision: + file_arg += "#" + revision + # -q suppresses the initial line that displays the filename and revision + return self.RunPerforceCommand(["print", "-q", file_arg], + universal_newlines=not is_binary) + + def GetBaseFilename(self, filename): + actionsWithDifferentBases = [ + "move/add", # p4 move + "branch", # p4 integrate (to a new file), similar to hg "add" + "add", # p4 integrate (to a new file), after modifying the new file + ] + + # We only see a different base for "add" if this is a downgraded branch + # after a file was branched (integrated), then edited. + if self.GetAction(filename) in actionsWithDifferentBases: + # -Or shows information about pending integrations/moves + fstat_result = self.RunPerforceCommand(["fstat", "-Or", filename], + marshal_output=True) + + baseFileKey = "resolveFromFile0" # I think it's safe to use only file0 + if baseFileKey in fstat_result: + return fstat_result[baseFileKey] + + return filename + + def GetBaseRevision(self, filename): + base_filename = self.GetBaseFilename(filename) + + have_result = self.RunPerforceCommand(["have", base_filename], + marshal_output=True) + if "haveRev" in have_result: + return have_result["haveRev"] + + def GetLocalFilename(self, filename): + where = self.RunPerforceCommand(["where", filename], marshal_output=True) + if "path" in where: + return where["path"] + + def GenerateDiff(self, args): + class DiffData: + def __init__(self, perforceVCS, filename, action): + self.perforceVCS = perforceVCS + self.filename = filename + self.action = action + self.base_filename = perforceVCS.GetBaseFilename(filename) + + self.file_body = None + self.base_rev = None + self.prefix = None + self.working_copy = True + self.change_summary = None + + def GenerateDiffHeader(diffData): + header = [] + header.append("Index: %s" % diffData.filename) + header.append("=" * 67) + + if diffData.base_filename != diffData.filename: + if diffData.action.startswith("move"): + verb = "rename" + else: + verb = "copy" + header.append("%s from %s" % (verb, diffData.base_filename)) + header.append("%s to %s" % (verb, diffData.filename)) + + suffix = "\t(revision %s)" % diffData.base_rev + header.append("--- " + diffData.base_filename + suffix) + if diffData.working_copy: + suffix = "\t(working copy)" + header.append("+++ " + diffData.filename + suffix) + if diffData.change_summary: + header.append(diffData.change_summary) + return header + + def GenerateMergeDiff(diffData, args): + # -du generates a unified diff, which is nearly svn format + diffData.file_body = self.RunPerforceCommand( + ["diff", "-du", diffData.filename] + args) + diffData.base_rev = self.GetBaseRevision(diffData.filename) + diffData.prefix = "" + + # We have to replace p4's file status output (the lines starting + # with +++ or ---) to match svn's diff format + lines = diffData.file_body.splitlines() + first_good_line = 0 + while (first_good_line < len(lines) and + not lines[first_good_line].startswith("@@")): + first_good_line += 1 + diffData.file_body = "\n".join(lines[first_good_line:]) + return diffData + + def GenerateAddDiff(diffData): + fstat = self.RunPerforceCommand(["fstat", diffData.filename], + marshal_output=True) + if "headRev" in fstat: + diffData.base_rev = fstat["headRev"] # Re-adding a deleted file + else: + diffData.base_rev = "0" # Brand new file + diffData.working_copy = False + rel_path = self.GetLocalFilename(diffData.filename) + diffData.file_body = open(rel_path, 'r').read() + # Replicate svn's list of changed lines + line_count = len(diffData.file_body.splitlines()) + diffData.change_summary = "@@ -0,0 +1" + if line_count > 1: + diffData.change_summary += ",%d" % line_count + diffData.change_summary += " @@" + diffData.prefix = "+" + return diffData + + def GenerateDeleteDiff(diffData): + diffData.base_rev = self.GetBaseRevision(diffData.filename) + is_base_binary = self.IsBaseBinary(diffData.filename) + # For deletes, base_filename == filename + diffData.file_body = self.GetFileContent(diffData.base_filename, + None, + is_base_binary) + # Replicate svn's list of changed lines + line_count = len(diffData.file_body.splitlines()) + diffData.change_summary = "@@ -1" + if line_count > 1: + diffData.change_summary += ",%d" % line_count + diffData.change_summary += " +0,0 @@" + diffData.prefix = "-" + return diffData + + changed_files = self.GetChangedFiles() + + svndiff = [] + filecount = 0 + for (filename, action) in changed_files.items(): + svn_status = self.PerforceActionToSvnStatus(action) + if svn_status == "SKIP": + continue + + diffData = DiffData(self, filename, action) + # Is it possible to diff a branched file? Stackoverflow says no: + # http://stackoverflow.com/questions/1771314/in-perforce-command-line-how-to-diff-a-file-reopened-for-add + if svn_status == "M": + diffData = GenerateMergeDiff(diffData, args) + elif svn_status == "A": + diffData = GenerateAddDiff(diffData) + elif svn_status == "D": + diffData = GenerateDeleteDiff(diffData) + else: + ErrorExit("Unknown file action %s (svn action %s)." % \ + (action, svn_status)) + + svndiff += GenerateDiffHeader(diffData) + + for line in diffData.file_body.splitlines(): + svndiff.append(diffData.prefix + line) + filecount += 1 + if not filecount: + ErrorExit("No valid patches found in output from p4 diff") + return "\n".join(svndiff) + "\n" + + def PerforceActionToSvnStatus(self, status): + # Mirroring the list at http://permalink.gmane.org/gmane.comp.version-control.mercurial.devel/28717 + # Is there something more official? + return { + "add" : "A", + "branch" : "A", + "delete" : "D", + "edit" : "M", # Also includes changing file types. + "integrate" : "M", + "move/add" : "M", + "move/delete": "SKIP", + "purge" : "D", # How does a file's status become "purge"? + }[status] + + def GetAction(self, filename): + changed_files = self.GetChangedFiles() + if not filename in changed_files: + ErrorExit("Trying to get base version of unknown file %s." % filename) + + return changed_files[filename] + + def GetBaseFile(self, filename): + base_filename = self.GetBaseFilename(filename) + base_content = "" + new_content = None + + status = self.PerforceActionToSvnStatus(self.GetAction(filename)) + + if status != "A": + revision = self.GetBaseRevision(base_filename) + if not revision: + ErrorExit("Couldn't find base revision for file %s" % filename) + is_base_binary = self.IsBaseBinary(base_filename) + base_content = self.GetFileContent(base_filename, + revision, + is_base_binary) + + is_binary = self.IsPendingBinary(filename) + if status != "D" and status != "SKIP": + relpath = self.GetLocalFilename(filename) + if is_binary and self.IsImage(relpath): + new_content = open(relpath, "rb").read() + + return base_content, new_content, is_binary, status + # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. def SplitPatch(data): """Splits a patch into separate pieces for each file. @@ -1433,7 +1819,7 @@ def UploadSeparatePatches(issue, rpc_server, patchset, data, options): return rv -def GuessVCSName(): +def GuessVCSName(options): """Helper to guess the version control system. This examines the current directory, guesses which VersionControlSystem @@ -1441,10 +1827,17 @@ def GuessVCSName(): Returns: A pair (vcs, output). vcs is a string indicating which VCS was detected - and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, or VCS_UNKNOWN. + and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, VCS_PERFORCE, + VCS_CVS, or VCS_UNKNOWN. + Since local perforce repositories can't be easily detected, this method + will only guess VCS_PERFORCE if any perforce options have been specified. output is a string containing any interesting output from the vcs detection routine, or None if there is nothing interesting. """ + for attribute, value in options.__dict__.iteritems(): + if attribute.startswith("p4") and value != None: + return (VCS_PERFORCE, None) + # 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. # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. @@ -1472,6 +1865,15 @@ def GuessVCSName(): if errno != 2: # ENOENT -- they don't have git installed. raise + # detect CVS repos use `cvs status && $? == 0` rules + try: + out, returncode = RunShellWithReturnCode(["cvs", "status"]) + if returncode == 0: + return (VCS_CVS, None) + except OSError, (errno, message): + if error != 2: + raise + return (VCS_UNKNOWN, None) @@ -1496,7 +1898,7 @@ def GuessVCS(options): ErrorExit("Unknown version control system %r specified." % vcs) (vcs, extra_output) = (v, None) else: - (vcs, extra_output) = GuessVCSName() + (vcs, extra_output) = GuessVCSName(options) if vcs == VCS_MERCURIAL: if extra_output is None: @@ -1504,8 +1906,12 @@ def GuessVCS(options): return MercurialVCS(options, extra_output) elif vcs == VCS_SUBVERSION: return SubversionVCS(options) + elif vcs == VCS_PERFORCE: + return PerforceVCS(options) elif vcs == VCS_GIT: return GitVCS(options) + elif vcs == VCS_CVS: + return CVSVCS(options) ErrorExit(("Could not guess version control system. " "Are you in a working copy directory?")) @@ -1684,6 +2090,10 @@ def RealMain(argv, data=None): if data is None: data = vcs.GenerateDiff(args) data = vcs.PostProcessDiff(data) + if options.print_diffs: + print "Rietveld diff start:*****" + print data + print "Rietveld diff end:*****" files = vcs.GetBaseFiles(data) if verbosity >= 1: print "Upload server:", options.server, "(change with -s/--server)" @@ -1701,6 +2111,12 @@ def RealMain(argv, data=None): options.account_type) form_fields = [("subject", message)] if base: + b = urlparse.urlparse(base) + username, netloc = urllib.splituser(b.netloc) + if username: + logging.info("Removed username from base URL") + base = urlparse.urlunparse((b.scheme, netloc, b.path, b.params, + b.query, b.fragment)) form_fields.append(("base", base)) if options.issue: form_fields.append(("issue", str(options.issue))) diff --git a/third_party/upload.py b/third_party/upload.py index ff7d3f33e..ee2b4c2b4 100755 --- a/third_party/upload.py +++ b/third_party/upload.py @@ -24,6 +24,8 @@ Supported version control systems: Git Mercurial Subversion + Perforce + CVS It is important for Git/Mercurial users to specify a tree/node/branch to diff against by using the '--rev' option. @@ -36,6 +38,7 @@ import cookielib import fnmatch import getpass import logging +import marshal import mimetypes import optparse import os @@ -86,6 +89,8 @@ MAX_UPLOAD_SIZE = 900 * 1024 VCS_GIT = "Git" VCS_MERCURIAL = "Mercurial" VCS_SUBVERSION = "Subversion" +VCS_PERFORCE = "Perforce" +VCS_CVS = "CVS" VCS_UNKNOWN = "Unknown" # whitelist for non-binary filetypes which do not start with "text/" @@ -99,7 +104,10 @@ VCS_ABBREVIATIONS = { "hg": VCS_MERCURIAL, VCS_SUBVERSION.lower(): VCS_SUBVERSION, "svn": VCS_SUBVERSION, + VCS_PERFORCE.lower(): VCS_PERFORCE, + "p4": VCS_PERFORCE, VCS_GIT.lower(): VCS_GIT, + VCS_CVS.lower(): VCS_CVS, } # The result of parsing Subversion's [auto-props] setting. @@ -188,8 +196,6 @@ class AbstractRpcServer(object): if (not self.host.startswith("http://") and not self.host.startswith("https://")): self.host = "http://" + self.host - assert re.match(r'^[a-z]+://[a-z0-9\.-_]+(|:[0-9]+)$', self.host), ( - '%s is malformed' % host) self.host_override = host_override self.auth_function = auth_function self.authenticated = False @@ -220,11 +226,10 @@ class AbstractRpcServer(object): req.add_header(key, value) return req - def _GetAuthToken(self, host, email, password): + def _GetAuthToken(self, email, password): """Uses ClientLogin to authenticate the user, returning an auth token. Args: - host: Host to get a token against. email: The user's email address password: The user's password @@ -236,7 +241,7 @@ class AbstractRpcServer(object): The authentication token returned by ClientLogin. """ account_type = self.account_type - if host.endswith(".google.com"): + if self.host.endswith(".google.com"): # Needed for use inside Google. account_type = "HOSTED" req = self._CreateRequest( @@ -264,12 +269,10 @@ class AbstractRpcServer(object): else: raise - def _GetAuthCookie(self, host, auth_token): + def _GetAuthCookie(self, auth_token): """Fetches authentication cookies for an authentication token. Args: - host: The host to get a cookie against. Because of 301, it may be a - different host than self.host. auth_token: The authentication token returned by ClientLogin. Raises: @@ -278,33 +281,21 @@ class AbstractRpcServer(object): # This is a dummy value to allow us to identify when we're successful. continue_location = "http://localhost/" args = {"continue": continue_location, "auth": auth_token} - tries = 0 - url = "%s/_ah/login?%s" % (host, urllib.urlencode(args)) - while tries < 3: - tries += 1 - req = self._CreateRequest(url) - try: - response = self.opener.open(req) - except urllib2.HTTPError, e: - response = e - if e.code == 301: - # Handle permanent redirect manually. - url = e.info()["location"] - continue - break + req = self._CreateRequest("%s/_ah/login?%s" % + (self.host, urllib.urlencode(args))) + try: + response = self.opener.open(req) + except urllib2.HTTPError, e: + response = e if (response.code != 302 or response.info()["location"] != continue_location): raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg, response.headers, response.fp) self.authenticated = True - def _Authenticate(self, host): + def _Authenticate(self): """Authenticates the user. - Args: - host: The host to get a cookie against. Because of 301, it may be a - different host than self.host. - The authentication process works as follows: 1) We get a username and password from the user 2) We use ClientLogin to obtain an AUTH token for the user @@ -320,7 +311,7 @@ class AbstractRpcServer(object): for i in range(3): credentials = self.auth_function() try: - auth_token = self._GetAuthToken(host, credentials[0], credentials[1]) + auth_token = self._GetAuthToken(credentials[0], credentials[1]) except ClientLoginError, e: if e.reason == "BadAuthentication": print >>sys.stderr, "Invalid username or password." @@ -353,7 +344,7 @@ class AbstractRpcServer(object): print >>sys.stderr, "The service is not available; try again later." break raise - self._GetAuthCookie(host, auth_token) + self._GetAuthCookie(auth_token) return def Send(self, request_path, payload=None, @@ -380,18 +371,18 @@ class AbstractRpcServer(object): # TODO: Don't require authentication. Let the server say # whether it is necessary. if not self.authenticated: - self._Authenticate(self.host) + self._Authenticate() old_timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(timeout) try: tries = 0 - args = dict(kwargs) - url = "%s%s" % (self.host, request_path) - if args: - url += "?" + urllib.urlencode(args) while True: tries += 1 + args = dict(kwargs) + url = "%s%s" % (self.host, request_path) + if args: + url += "?" + urllib.urlencode(args) req = self._CreateRequest(url=url, data=payload) req.add_header("Content-Type", content_type) if extra_headers: @@ -406,24 +397,17 @@ class AbstractRpcServer(object): if tries > 3: raise elif e.code == 401 or e.code == 302: - url_loc = urlparse.urlparse(url) - self._Authenticate('%s://%s' % (url_loc[0], url_loc[1])) + self._Authenticate() ## elif e.code >= 500 and e.code < 600: ## # Server Error - try again. ## continue elif e.code == 301: # Handle permanent redirect manually. url = e.info()["location"] + url_loc = urlparse.urlparse(url) + self.host = '%s://%s' % (url_loc[0], url_loc[1]) else: raise - except urllib2.URLError, e: - reason = getattr(e, 'reason', None) - if isinstance(reason, str) and reason.find("110") != -1: - # Connection timeout error. - if tries <= 3: - # Try again. - continue - raise finally: socket.setdefaulttimeout(old_timeout) @@ -431,9 +415,9 @@ class AbstractRpcServer(object): class HttpRpcServer(AbstractRpcServer): """Provides a simplified RPC-style interface for HTTP requests.""" - def _Authenticate(self, *args): + def _Authenticate(self): """Save the cookie jar after authentication.""" - super(HttpRpcServer, self)._Authenticate(*args) + super(HttpRpcServer, self)._Authenticate() if self.save_cookies: StatusUpdate("Saving authentication cookies to %s" % self.cookie_file) self.cookie_jar.save() @@ -490,6 +474,8 @@ group.add_option("-v", "--verbose", action="store_const", const=2, help="Print info level logs.") group.add_option("--noisy", action="store_const", const=3, dest="verbose", help="Print all logs.") +group.add_option("--print_diffs", dest="print_diffs", action="store_true", + help="Print full diffs.") # Review server group = parser.add_option_group("Review server options") group.add_option("-s", "--server", action="store", dest="server", @@ -562,7 +548,21 @@ group.add_option("--vcs", action="store", dest="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.")) - +# Perforce-specific +group = parser.add_option_group("Perforce-specific options " + "(overrides P4 environment variables)") +group.add_option("--p4_port", action="store", dest="p4_port", + metavar="P4_PORT", default=None, + help=("Perforce server and port (optional)")) +group.add_option("--p4_changelist", action="store", dest="p4_changelist", + metavar="P4_CHANGELIST", default=None, + help=("Perforce changelist id")) +group.add_option("--p4_client", action="store", dest="p4_client", + metavar="P4_CLIENT", default=None, + help=("Perforce client/workspace")) +group.add_option("--p4_user", action="store", dest="p4_user", + metavar="P4_USER", default=None, + help=("Perforce user")) def GetRpcServer(server, email=None, host_override=None, save_cookies=True, account_type=AUTH_ACCOUNT_TYPE): @@ -908,9 +908,6 @@ class SubversionVCS(VersionControlSystem): if line.startswith("URL: "): url = line.split()[1] scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) - username, netloc = urllib.splituser(netloc) - if username: - logging.info("Removed username from base URL") guess = "" if netloc == "svn.python.org" and scheme == "svn+ssh": path = "projects" + path @@ -1070,8 +1067,12 @@ class SubversionVCS(VersionControlSystem): # File does not exist in the requested revision. # Reset mimetype, it contains an error message. mimetype = "" + else: + mimetype = mimetype.strip() get_base = False - is_binary = bool(mimetype) and not mimetype.startswith("text/") + is_binary = (bool(mimetype) and + not mimetype.startswith("text/") and + not mimetype in TEXT_MIMETYPES) if status[0] == " ": # Empty base content just to force an upload. base_content = "" @@ -1265,6 +1266,71 @@ class GitVCS(VersionControlSystem): return (base_content, new_content, is_binary, status) +class CVSVCS(VersionControlSystem): + """Implementation of the VersionControlSystem interface for CVS.""" + + def __init__(self, options): + super(CVSVCS, self).__init__(options) + + def GetOriginalContent_(self, filename): + RunShell(["cvs", "up", filename], silent_ok=True) + # TODO need detect file content encoding + content = open(filename).read() + return content.replace("\r\n", "\n") + + def GetBaseFile(self, filename): + base_content = None + new_content = None + is_binary = False + status = "A" + + output, retcode = RunShellWithReturnCode(["cvs", "status", filename]) + if retcode: + ErrorExit("Got error status from 'cvs status %s'" % filename) + + if output.find("Status: Locally Modified") != -1: + status = "M" + temp_filename = "%s.tmp123" % filename + os.rename(filename, temp_filename) + base_content = self.GetOriginalContent_(filename) + os.rename(temp_filename, filename) + elif output.find("Status: Locally Added"): + status = "A" + base_content = "" + elif output.find("Status: Needs Checkout"): + status = "D" + base_content = self.GetOriginalContent_(filename) + + return (base_content, new_content, is_binary, status) + + def GenerateDiff(self, extra_args): + cmd = ["cvs", "diff", "-u", "-N"] + if self.options.revision: + cmd += ["-r", self.options.revision] + + cmd.extend(extra_args) + data, retcode = RunShellWithReturnCode(cmd) + count = 0 + if retcode == 0: + for line in data.splitlines(): + if line.startswith("Index:"): + count += 1 + logging.info(line) + + if not count: + ErrorExit("No valid patches found in output from cvs diff") + + return data + + def GetUnknownFiles(self): + status = RunShell(["cvs", "diff"], + silent_ok=True) + unknown_files = [] + for line in status.split("\n"): + if line and line[0] == "?": + unknown_files.append(line) + return unknown_files + class MercurialVCS(VersionControlSystem): """Implementation of the VersionControlSystem interface for Mercurial.""" @@ -1364,6 +1430,326 @@ class MercurialVCS(VersionControlSystem): return base_content, new_content, is_binary, status +class PerforceVCS(VersionControlSystem): + """Implementation of the VersionControlSystem interface for Perforce.""" + + def __init__(self, options): + + def ConfirmLogin(): + # Make sure we have a valid perforce session + while True: + data, retcode = self.RunPerforceCommandWithReturnCode( + ["login", "-s"], marshal_output=True) + if not data: + ErrorExit("Error checking perforce login") + if not retcode and (not "code" in data or data["code"] != "error"): + break + print "Enter perforce password: " + self.RunPerforceCommandWithReturnCode(["login"]) + + super(PerforceVCS, self).__init__(options) + + self.p4_changelist = options.p4_changelist + if not self.p4_changelist: + ErrorExit("A changelist id is required") + if (options.revision): + ErrorExit("--rev is not supported for perforce") + + self.p4_port = options.p4_port + self.p4_client = options.p4_client + self.p4_user = options.p4_user + + ConfirmLogin() + + if not options.message: + description = self.RunPerforceCommand(["describe", self.p4_changelist], + marshal_output=True) + if description and "desc" in description: + # Rietveld doesn't support multi-line descriptions + raw_message = description["desc"].strip() + lines = raw_message.splitlines() + if len(lines): + options.message = lines[0] + + def RunPerforceCommandWithReturnCode(self, extra_args, marshal_output=False, + universal_newlines=True): + args = ["p4"] + if marshal_output: + # -G makes perforce format its output as marshalled python objects + args.extend(["-G"]) + if self.p4_port: + args.extend(["-p", self.p4_port]) + if self.p4_client: + args.extend(["-c", self.p4_client]) + if self.p4_user: + args.extend(["-u", self.p4_user]) + args.extend(extra_args) + + data, retcode = RunShellWithReturnCode( + args, print_output=False, universal_newlines=universal_newlines) + if marshal_output and data: + data = marshal.loads(data) + return data, retcode + + def RunPerforceCommand(self, extra_args, marshal_output=False, + universal_newlines=True): + # This might be a good place to cache call results, since things like + # describe or fstat might get called repeatedly. + data, retcode = self.RunPerforceCommandWithReturnCode( + extra_args, marshal_output, universal_newlines) + if retcode: + ErrorExit("Got error status from %s:\n%s" % (extra_args, data)) + return data + + def GetFileProperties(self, property_key_prefix = "", command = "describe"): + description = self.RunPerforceCommand(["describe", self.p4_changelist], + marshal_output=True) + + changed_files = {} + file_index = 0 + # Try depotFile0, depotFile1, ... until we don't find a match + while True: + file_key = "depotFile%d" % file_index + if file_key in description: + filename = description[file_key] + change_type = description[property_key_prefix + str(file_index)] + changed_files[filename] = change_type + file_index += 1 + else: + break + return changed_files + + def GetChangedFiles(self): + return self.GetFileProperties("action") + + def GetUnknownFiles(self): + # Perforce doesn't detect new files, they have to be explicitly added + return [] + + def IsBaseBinary(self, filename): + base_filename = self.GetBaseFilename(filename) + return self.IsBinaryHelper(base_filename, "files") + + def IsPendingBinary(self, filename): + return self.IsBinaryHelper(filename, "describe") + + def IsBinary(self, filename): + ErrorExit("IsBinary is not safe: call IsBaseBinary or IsPendingBinary") + + def IsBinaryHelper(self, filename, command): + file_types = self.GetFileProperties("type", command) + if not filename in file_types: + ErrorExit("Trying to check binary status of unknown file %s." % filename) + # This treats symlinks, macintosh resource files, temporary objects, and + # unicode as binary. See the Perforce docs for more details: + # http://www.perforce.com/perforce/doc.current/manuals/cmdref/o.ftypes.html + return not file_types[filename].endswith("text") + + def GetFileContent(self, filename, revision, is_binary): + file_arg = filename + if revision: + file_arg += "#" + revision + # -q suppresses the initial line that displays the filename and revision + return self.RunPerforceCommand(["print", "-q", file_arg], + universal_newlines=not is_binary) + + def GetBaseFilename(self, filename): + actionsWithDifferentBases = [ + "move/add", # p4 move + "branch", # p4 integrate (to a new file), similar to hg "add" + "add", # p4 integrate (to a new file), after modifying the new file + ] + + # We only see a different base for "add" if this is a downgraded branch + # after a file was branched (integrated), then edited. + if self.GetAction(filename) in actionsWithDifferentBases: + # -Or shows information about pending integrations/moves + fstat_result = self.RunPerforceCommand(["fstat", "-Or", filename], + marshal_output=True) + + baseFileKey = "resolveFromFile0" # I think it's safe to use only file0 + if baseFileKey in fstat_result: + return fstat_result[baseFileKey] + + return filename + + def GetBaseRevision(self, filename): + base_filename = self.GetBaseFilename(filename) + + have_result = self.RunPerforceCommand(["have", base_filename], + marshal_output=True) + if "haveRev" in have_result: + return have_result["haveRev"] + + def GetLocalFilename(self, filename): + where = self.RunPerforceCommand(["where", filename], marshal_output=True) + if "path" in where: + return where["path"] + + def GenerateDiff(self, args): + class DiffData: + def __init__(self, perforceVCS, filename, action): + self.perforceVCS = perforceVCS + self.filename = filename + self.action = action + self.base_filename = perforceVCS.GetBaseFilename(filename) + + self.file_body = None + self.base_rev = None + self.prefix = None + self.working_copy = True + self.change_summary = None + + def GenerateDiffHeader(diffData): + header = [] + header.append("Index: %s" % diffData.filename) + header.append("=" * 67) + + if diffData.base_filename != diffData.filename: + if diffData.action.startswith("move"): + verb = "rename" + else: + verb = "copy" + header.append("%s from %s" % (verb, diffData.base_filename)) + header.append("%s to %s" % (verb, diffData.filename)) + + suffix = "\t(revision %s)" % diffData.base_rev + header.append("--- " + diffData.base_filename + suffix) + if diffData.working_copy: + suffix = "\t(working copy)" + header.append("+++ " + diffData.filename + suffix) + if diffData.change_summary: + header.append(diffData.change_summary) + return header + + def GenerateMergeDiff(diffData, args): + # -du generates a unified diff, which is nearly svn format + diffData.file_body = self.RunPerforceCommand( + ["diff", "-du", diffData.filename] + args) + diffData.base_rev = self.GetBaseRevision(diffData.filename) + diffData.prefix = "" + + # We have to replace p4's file status output (the lines starting + # with +++ or ---) to match svn's diff format + lines = diffData.file_body.splitlines() + first_good_line = 0 + while (first_good_line < len(lines) and + not lines[first_good_line].startswith("@@")): + first_good_line += 1 + diffData.file_body = "\n".join(lines[first_good_line:]) + return diffData + + def GenerateAddDiff(diffData): + fstat = self.RunPerforceCommand(["fstat", diffData.filename], + marshal_output=True) + if "headRev" in fstat: + diffData.base_rev = fstat["headRev"] # Re-adding a deleted file + else: + diffData.base_rev = "0" # Brand new file + diffData.working_copy = False + rel_path = self.GetLocalFilename(diffData.filename) + diffData.file_body = open(rel_path, 'r').read() + # Replicate svn's list of changed lines + line_count = len(diffData.file_body.splitlines()) + diffData.change_summary = "@@ -0,0 +1" + if line_count > 1: + diffData.change_summary += ",%d" % line_count + diffData.change_summary += " @@" + diffData.prefix = "+" + return diffData + + def GenerateDeleteDiff(diffData): + diffData.base_rev = self.GetBaseRevision(diffData.filename) + is_base_binary = self.IsBaseBinary(diffData.filename) + # For deletes, base_filename == filename + diffData.file_body = self.GetFileContent(diffData.base_filename, + None, + is_base_binary) + # Replicate svn's list of changed lines + line_count = len(diffData.file_body.splitlines()) + diffData.change_summary = "@@ -1" + if line_count > 1: + diffData.change_summary += ",%d" % line_count + diffData.change_summary += " +0,0 @@" + diffData.prefix = "-" + return diffData + + changed_files = self.GetChangedFiles() + + svndiff = [] + filecount = 0 + for (filename, action) in changed_files.items(): + svn_status = self.PerforceActionToSvnStatus(action) + if svn_status == "SKIP": + continue + + diffData = DiffData(self, filename, action) + # Is it possible to diff a branched file? Stackoverflow says no: + # http://stackoverflow.com/questions/1771314/in-perforce-command-line-how-to-diff-a-file-reopened-for-add + if svn_status == "M": + diffData = GenerateMergeDiff(diffData, args) + elif svn_status == "A": + diffData = GenerateAddDiff(diffData) + elif svn_status == "D": + diffData = GenerateDeleteDiff(diffData) + else: + ErrorExit("Unknown file action %s (svn action %s)." % \ + (action, svn_status)) + + svndiff += GenerateDiffHeader(diffData) + + for line in diffData.file_body.splitlines(): + svndiff.append(diffData.prefix + line) + filecount += 1 + if not filecount: + ErrorExit("No valid patches found in output from p4 diff") + return "\n".join(svndiff) + "\n" + + def PerforceActionToSvnStatus(self, status): + # Mirroring the list at http://permalink.gmane.org/gmane.comp.version-control.mercurial.devel/28717 + # Is there something more official? + return { + "add" : "A", + "branch" : "A", + "delete" : "D", + "edit" : "M", # Also includes changing file types. + "integrate" : "M", + "move/add" : "M", + "move/delete": "SKIP", + "purge" : "D", # How does a file's status become "purge"? + }[status] + + def GetAction(self, filename): + changed_files = self.GetChangedFiles() + if not filename in changed_files: + ErrorExit("Trying to get base version of unknown file %s." % filename) + + return changed_files[filename] + + def GetBaseFile(self, filename): + base_filename = self.GetBaseFilename(filename) + base_content = "" + new_content = None + + status = self.PerforceActionToSvnStatus(self.GetAction(filename)) + + if status != "A": + revision = self.GetBaseRevision(base_filename) + if not revision: + ErrorExit("Couldn't find base revision for file %s" % filename) + is_base_binary = self.IsBaseBinary(base_filename) + base_content = self.GetFileContent(base_filename, + revision, + is_base_binary) + + is_binary = self.IsPendingBinary(filename) + if status != "D" and status != "SKIP": + relpath = self.GetLocalFilename(filename) + if is_binary and self.IsImage(relpath): + new_content = open(relpath, "rb").read() + + return base_content, new_content, is_binary, status + # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync. def SplitPatch(data): """Splits a patch into separate pieces for each file. @@ -1433,7 +1819,7 @@ def UploadSeparatePatches(issue, rpc_server, patchset, data, options): return rv -def GuessVCSName(): +def GuessVCSName(options): """Helper to guess the version control system. This examines the current directory, guesses which VersionControlSystem @@ -1441,10 +1827,17 @@ def GuessVCSName(): Returns: A pair (vcs, output). vcs is a string indicating which VCS was detected - and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, or VCS_UNKNOWN. + and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, VCS_PERFORCE, + VCS_CVS, or VCS_UNKNOWN. + Since local perforce repositories can't be easily detected, this method + will only guess VCS_PERFORCE if any perforce options have been specified. output is a string containing any interesting output from the vcs detection routine, or None if there is nothing interesting. """ + for attribute, value in options.__dict__.iteritems(): + if attribute.startswith("p4") and value != None: + return (VCS_PERFORCE, None) + # 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. # NOTE: we try Mercurial first as it can sit on top of an SVN working copy. @@ -1472,6 +1865,15 @@ def GuessVCSName(): if errno != 2: # ENOENT -- they don't have git installed. raise + # detect CVS repos use `cvs status && $? == 0` rules + try: + out, returncode = RunShellWithReturnCode(["cvs", "status"]) + if returncode == 0: + return (VCS_CVS, None) + except OSError, (errno, message): + if error != 2: + raise + return (VCS_UNKNOWN, None) @@ -1496,7 +1898,7 @@ def GuessVCS(options): ErrorExit("Unknown version control system %r specified." % vcs) (vcs, extra_output) = (v, None) else: - (vcs, extra_output) = GuessVCSName() + (vcs, extra_output) = GuessVCSName(options) if vcs == VCS_MERCURIAL: if extra_output is None: @@ -1504,8 +1906,12 @@ def GuessVCS(options): return MercurialVCS(options, extra_output) elif vcs == VCS_SUBVERSION: return SubversionVCS(options) + elif vcs == VCS_PERFORCE: + return PerforceVCS(options) elif vcs == VCS_GIT: return GitVCS(options) + elif vcs == VCS_CVS: + return CVSVCS(options) ErrorExit(("Could not guess version control system. " "Are you in a working copy directory?")) @@ -1684,6 +2090,10 @@ def RealMain(argv, data=None): if data is None: data = vcs.GenerateDiff(args) data = vcs.PostProcessDiff(data) + if options.print_diffs: + print "Rietveld diff start:*****" + print data + print "Rietveld diff end:*****" files = vcs.GetBaseFiles(data) if verbosity >= 1: print "Upload server:", options.server, "(change with -s/--server)" @@ -1701,6 +2111,12 @@ def RealMain(argv, data=None): options.account_type) form_fields = [("subject", message)] if base: + b = urlparse.urlparse(base) + username, netloc = urllib.splituser(b.netloc) + if username: + logging.info("Removed username from base URL") + base = urlparse.urlunparse((b.scheme, netloc, b.path, b.params, + b.query, b.fragment)) form_fields.append(("base", base)) if options.issue: form_fields.append(("issue", str(options.issue)))