subversion.py 9.4 KB

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