| Index: third_party/upload.py | 
| diff --git a/third_party/upload.py b/third_party/upload.py | 
| index ff7d3f33e750518f3bcf0dd2ae7a5ef67401d3a8..ee2b4c2b4c4432bf84bee80226abc32bc27c2305 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))) | 
|  |