1from __future__ import absolute_import 2 3import logging 4import os 5import re 6 7from pip9._vendor.six.moves.urllib import parse as urllib_parse 8 9from pip9.index import Link 10from pip9.utils import rmtree, display_path 11from pip9.utils.logging import indent_log 12from pip9.vcs import vcs, VersionControl 13 14_svn_xml_url_re = re.compile('url="([^"]+)"') 15_svn_rev_re = re.compile('committed-rev="(\d+)"') 16_svn_url_re = re.compile(r'URL: (.+)') 17_svn_revision_re = re.compile(r'Revision: (.+)') 18_svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"') 19_svn_info_xml_url_re = re.compile(r'<url>(.*)</url>') 20 21 22logger = logging.getLogger(__name__) 23 24 25class Subversion(VersionControl): 26 name = 'svn' 27 dirname = '.svn' 28 repo_name = 'checkout' 29 schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn') 30 31 def get_info(self, location): 32 """Returns (url, revision), where both are strings""" 33 assert not location.rstrip('/').endswith(self.dirname), \ 34 'Bad directory: %s' % location 35 output = self.run_command( 36 ['info', location], 37 show_stdout=False, 38 extra_environ={'LANG': 'C'}, 39 ) 40 match = _svn_url_re.search(output) 41 if not match: 42 logger.warning( 43 'Cannot determine URL of svn checkout %s', 44 display_path(location), 45 ) 46 logger.debug('Output that cannot be parsed: \n%s', output) 47 return None, None 48 url = match.group(1).strip() 49 match = _svn_revision_re.search(output) 50 if not match: 51 logger.warning( 52 'Cannot determine revision of svn checkout %s', 53 display_path(location), 54 ) 55 logger.debug('Output that cannot be parsed: \n%s', output) 56 return url, None 57 return url, match.group(1) 58 59 def export(self, location): 60 """Export the svn repository at the url to the destination location""" 61 url, rev = self.get_url_rev() 62 rev_options = get_rev_options(url, rev) 63 url = self.remove_auth_from_url(url) 64 logger.info('Exporting svn repository %s to %s', url, location) 65 with indent_log(): 66 if os.path.exists(location): 67 # Subversion doesn't like to check out over an existing 68 # directory --force fixes this, but was only added in svn 1.5 69 rmtree(location) 70 self.run_command( 71 ['export'] + rev_options + [url, location], 72 show_stdout=False) 73 74 def switch(self, dest, url, rev_options): 75 self.run_command(['switch'] + rev_options + [url, dest]) 76 77 def update(self, dest, rev_options): 78 self.run_command(['update'] + rev_options + [dest]) 79 80 def obtain(self, dest): 81 url, rev = self.get_url_rev() 82 rev_options = get_rev_options(url, rev) 83 url = self.remove_auth_from_url(url) 84 if rev: 85 rev_display = ' (to revision %s)' % rev 86 else: 87 rev_display = '' 88 if self.check_destination(dest, url, rev_options, rev_display): 89 logger.info( 90 'Checking out %s%s to %s', 91 url, 92 rev_display, 93 display_path(dest), 94 ) 95 self.run_command(['checkout', '-q'] + rev_options + [url, dest]) 96 97 def get_location(self, dist, dependency_links): 98 for url in dependency_links: 99 egg_fragment = Link(url).egg_fragment 100 if not egg_fragment: 101 continue 102 if '-' in egg_fragment: 103 # FIXME: will this work when a package has - in the name? 104 key = '-'.join(egg_fragment.split('-')[:-1]).lower() 105 else: 106 key = egg_fragment 107 if key == dist.key: 108 return url.split('#', 1)[0] 109 return None 110 111 def get_revision(self, location): 112 """ 113 Return the maximum revision for all files under a given location 114 """ 115 # Note: taken from setuptools.command.egg_info 116 revision = 0 117 118 for base, dirs, files in os.walk(location): 119 if self.dirname not in dirs: 120 dirs[:] = [] 121 continue # no sense walking uncontrolled subdirs 122 dirs.remove(self.dirname) 123 entries_fn = os.path.join(base, self.dirname, 'entries') 124 if not os.path.exists(entries_fn): 125 # FIXME: should we warn? 126 continue 127 128 dirurl, localrev = self._get_svn_url_rev(base) 129 130 if base == location: 131 base_url = dirurl + '/' # save the root url 132 elif not dirurl or not dirurl.startswith(base_url): 133 dirs[:] = [] 134 continue # not part of the same svn tree, skip it 135 revision = max(revision, localrev) 136 return revision 137 138 def get_url_rev(self): 139 # hotfix the URL scheme after removing svn+ from svn+ssh:// readd it 140 url, rev = super(Subversion, self).get_url_rev() 141 if url.startswith('ssh://'): 142 url = 'svn+' + url 143 return url, rev 144 145 def get_url(self, location): 146 # In cases where the source is in a subdirectory, not alongside 147 # setup.py we have to look up in the location until we find a real 148 # setup.py 149 orig_location = location 150 while not os.path.exists(os.path.join(location, 'setup.py')): 151 last_location = location 152 location = os.path.dirname(location) 153 if location == last_location: 154 # We've traversed up to the root of the filesystem without 155 # finding setup.py 156 logger.warning( 157 "Could not find setup.py for directory %s (tried all " 158 "parent directories)", 159 orig_location, 160 ) 161 return None 162 163 return self._get_svn_url_rev(location)[0] 164 165 def _get_svn_url_rev(self, location): 166 from pip9.exceptions import InstallationError 167 168 entries_path = os.path.join(location, self.dirname, 'entries') 169 if os.path.exists(entries_path): 170 with open(entries_path) as f: 171 data = f.read() 172 else: # subversion >= 1.7 does not have the 'entries' file 173 data = '' 174 175 if (data.startswith('8') or 176 data.startswith('9') or 177 data.startswith('10')): 178 data = list(map(str.splitlines, data.split('\n\x0c\n'))) 179 del data[0][0] # get rid of the '8' 180 url = data[0][3] 181 revs = [int(d[9]) for d in data if len(d) > 9 and d[9]] + [0] 182 elif data.startswith('<?xml'): 183 match = _svn_xml_url_re.search(data) 184 if not match: 185 raise ValueError('Badly formatted data: %r' % data) 186 url = match.group(1) # get repository URL 187 revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0] 188 else: 189 try: 190 # subversion >= 1.7 191 xml = self.run_command( 192 ['info', '--xml', location], 193 show_stdout=False, 194 ) 195 url = _svn_info_xml_url_re.search(xml).group(1) 196 revs = [ 197 int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml) 198 ] 199 except InstallationError: 200 url, revs = None, [] 201 202 if revs: 203 rev = max(revs) 204 else: 205 rev = 0 206 207 return url, rev 208 209 def get_src_requirement(self, dist, location): 210 repo = self.get_url(location) 211 if repo is None: 212 return None 213 # FIXME: why not project name? 214 egg_project_name = dist.egg_name().split('-', 1)[0] 215 rev = self.get_revision(location) 216 return 'svn+%s@%s#egg=%s' % (repo, rev, egg_project_name) 217 218 def check_version(self, dest, rev_options): 219 """Always assume the versions don't match""" 220 return False 221 222 @staticmethod 223 def remove_auth_from_url(url): 224 # Return a copy of url with 'username:password@' removed. 225 # username/pass params are passed to subversion through flags 226 # and are not recognized in the url. 227 228 # parsed url 229 purl = urllib_parse.urlsplit(url) 230 stripped_netloc = \ 231 purl.netloc.split('@')[-1] 232 233 # stripped url 234 url_pieces = ( 235 purl.scheme, stripped_netloc, purl.path, purl.query, purl.fragment 236 ) 237 surl = urllib_parse.urlunsplit(url_pieces) 238 return surl 239 240 241def get_rev_options(url, rev): 242 if rev: 243 rev_options = ['-r', rev] 244 else: 245 rev_options = [] 246 247 r = urllib_parse.urlsplit(url) 248 if hasattr(r, 'username'): 249 # >= Python-2.5 250 username, password = r.username, r.password 251 else: 252 netloc = r[1] 253 if '@' in netloc: 254 auth = netloc.split('@')[0] 255 if ':' in auth: 256 username, password = auth.split(':', 1) 257 else: 258 username, password = auth, None 259 else: 260 username, password = None, None 261 262 if username: 263 rev_options += ['--username', username] 264 if password: 265 rev_options += ['--password', password] 266 return rev_options 267 268 269vcs.register(Subversion) 270