Home | History | Annotate | Download | only in scm
      1 # Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
      2 # Copyright (c) 2009 Apple Inc. All rights reserved.
      3 #
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 #
      8 #     * Redistributions of source code must retain the above copyright
      9 # notice, this list of conditions and the following disclaimer.
     10 #     * Redistributions in binary form must reproduce the above
     11 # copyright notice, this list of conditions and the following disclaimer
     12 # in the documentation and/or other materials provided with the
     13 # distribution.
     14 #     * Neither the name of Google Inc. nor the names of its
     15 # contributors may be used to endorse or promote products derived from
     16 # this software without specific prior written permission.
     17 #
     18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 import logging
     31 import os
     32 import random
     33 import re
     34 import shutil
     35 import string
     36 import sys
     37 import tempfile
     38 
     39 from webkitpy.common.memoized import memoized
     40 from webkitpy.common.system.executive import Executive, ScriptError
     41 
     42 from .scm import SCM
     43 
     44 _log = logging.getLogger(__name__)
     45 
     46 
     47 class SVN(SCM):
     48 
     49     executable_name = "svn"
     50 
     51     _svn_metadata_files = frozenset(['.svn', '_svn'])
     52 
     53     def __init__(self, cwd, patch_directories, **kwargs):
     54         SCM.__init__(self, cwd, **kwargs)
     55         self._bogus_dir = None
     56         if patch_directories == []:
     57             raise Exception(message='Empty list of patch directories passed to SCM.__init__')
     58         elif patch_directories == None:
     59             self._patch_directories = [self._filesystem.relpath(cwd, self.checkout_root)]
     60         else:
     61             self._patch_directories = patch_directories
     62 
     63     @classmethod
     64     def in_working_directory(cls, path, executive=None):
     65         if os.path.isdir(os.path.join(path, '.svn')):
     66             # This is a fast shortcut for svn info that is usually correct for SVN < 1.7,
     67             # but doesn't work for SVN >= 1.7.
     68             return True
     69 
     70         executive = executive or Executive()
     71         svn_info_args = [cls.executable_name, 'info']
     72         exit_code = executive.run_command(svn_info_args, cwd=path, return_exit_code=True)
     73         return (exit_code == 0)
     74 
     75     def _find_uuid(self, path):
     76         if not self.in_working_directory(path):
     77             return None
     78         return self.value_from_svn_info(path, 'Repository UUID')
     79 
     80     @classmethod
     81     def value_from_svn_info(cls, path, field_name):
     82         svn_info_args = [cls.executable_name, 'info']
     83         # FIXME: This method should use a passed in executive or be made an instance method and use self._executive.
     84         info_output = Executive().run_command(svn_info_args, cwd=path).rstrip()
     85         match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)
     86         if not match:
     87             raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)
     88         return match.group('value').rstrip('\r')
     89 
     90     def find_checkout_root(self, path):
     91         uuid = self._find_uuid(path)
     92         # If |path| is not in a working directory, we're supposed to return |path|.
     93         if not uuid:
     94             return path
     95         # Search up the directory hierarchy until we find a different UUID.
     96         last_path = None
     97         while True:
     98             if uuid != self._find_uuid(path):
     99                 return last_path
    100             last_path = path
    101             (path, last_component) = self._filesystem.split(path)
    102             if last_path == path:
    103                 return None
    104 
    105     def _run_svn(self, args, **kwargs):
    106         return self._run([self.executable_name] + args, **kwargs)
    107 
    108     @memoized
    109     def _svn_version(self):
    110         return self._run_svn(['--version', '--quiet'])
    111 
    112     def has_working_directory_changes(self):
    113         # FIXME: What about files which are not committed yet?
    114         return self._run_svn(["diff"], cwd=self.checkout_root, decode_output=False) != ""
    115 
    116     def status_command(self):
    117         return [self.executable_name, 'status']
    118 
    119     def _status_regexp(self, expected_types):
    120         field_count = 6 if self._svn_version() > "1.6" else 5
    121         return "^(?P<status>[%s]).{%s} (?P<filename>.+)$" % (expected_types, field_count)
    122 
    123     def _add_parent_directories(self, path, recurse):
    124         """Does 'svn add' to the path and its parents."""
    125         if self.in_working_directory(path):
    126             return
    127         self.add(path, recurse=recurse)
    128 
    129     def add_list(self, paths, return_exit_code=False, recurse=True):
    130         for path in paths:
    131             self._add_parent_directories(os.path.dirname(os.path.abspath(path)),
    132                                          recurse=False)
    133         if recurse:
    134             cmd = ["add"] + paths
    135         else:
    136             cmd = ["add", "--depth", "empty"] + paths
    137         return self._run_svn(cmd, return_exit_code=return_exit_code)
    138 
    139     def _delete_parent_directories(self, path):
    140         if not self.in_working_directory(path):
    141             return
    142         if set(os.listdir(path)) - self._svn_metadata_files:
    143             return  # Directory has non-trivial files in it.
    144         self.delete(path)
    145 
    146     def delete_list(self, paths):
    147         for path in paths:
    148             abs_path = os.path.abspath(path)
    149             parent, base = os.path.split(abs_path)
    150             result = self._run_svn(["delete", "--force", base], cwd=parent)
    151             self._delete_parent_directories(os.path.dirname(abs_path))
    152         return result
    153 
    154     def move(self, origin, destination):
    155         return self._run_svn(["mv", "--force", origin, destination], return_exit_code=True)
    156 
    157     def exists(self, path):
    158         return not self._run_svn(["info", path], return_exit_code=True, decode_output=False)
    159 
    160     def changed_files(self, git_commit=None):
    161         status_command = [self.executable_name, "status"]
    162         status_command.extend(self._patch_directories)
    163         # ACDMR: Addded, Conflicted, Deleted, Modified or Replaced
    164         return self._run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR"))
    165 
    166     def _added_files(self):
    167         return self._run_status_and_extract_filenames(self.status_command(), self._status_regexp("A"))
    168 
    169     def _deleted_files(self):
    170         return self._run_status_and_extract_filenames(self.status_command(), self._status_regexp("D"))
    171 
    172     @staticmethod
    173     def supports_local_commits():
    174         return False
    175 
    176     def display_name(self):
    177         return "svn"
    178 
    179     def svn_revision(self, path):
    180         return self.value_from_svn_info(path, 'Revision')
    181 
    182     def timestamp_of_revision(self, path, revision):
    183         # We use --xml to get timestamps like 2013-02-08T08:18:04.964409Z
    184         repository_root = self.value_from_svn_info(self.checkout_root, 'Repository Root')
    185         info_output = Executive().run_command([self.executable_name, 'log', '-r', revision, '--xml', repository_root], cwd=path).rstrip()
    186         match = re.search(r"^<date>(?P<value>.+)</date>\r?$", info_output, re.MULTILINE)
    187         return match.group('value')
    188 
    189     def create_patch(self, git_commit=None, changed_files=None):
    190         """Returns a byte array (str()) representing the patch file.
    191         Patch files are effectively binary since they may contain
    192         files of multiple different encodings."""
    193         if changed_files == []:
    194             return ""
    195         elif changed_files == None:
    196             changed_files = []
    197         return self._run([self._filesystem.join(self.checkout_root, 'Tools', 'Scripts', 'svn-create-patch')] + changed_files,
    198             cwd=self.checkout_root, return_stderr=False,
    199             decode_output=False)
    200 
    201     def blame(self, path):
    202         return self._run_svn(['blame', path])
    203