subversion.py 9.3 KB

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