You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

345 lines
12 KiB

  1. """Handles all VCS (version control) support"""
  2. from __future__ import absolute_import
  3. import errno
  4. import logging
  5. import os
  6. import shutil
  7. from pip._vendor.six.moves.urllib import parse as urllib_parse
  8. from pip.exceptions import BadCommand
  9. from pip.utils import (display_path, backup_dir, call_subprocess,
  10. rmtree, ask_path_exists)
  11. __all__ = ['vcs', 'get_src_requirement']
  12. logger = logging.getLogger(__name__)
  13. class VcsSupport(object):
  14. _registry = {}
  15. schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
  16. def __init__(self):
  17. # Register more schemes with urlparse for various version control
  18. # systems
  19. urllib_parse.uses_netloc.extend(self.schemes)
  20. # Python >= 2.7.4, 3.3 doesn't have uses_fragment
  21. if getattr(urllib_parse, 'uses_fragment', None):
  22. urllib_parse.uses_fragment.extend(self.schemes)
  23. super(VcsSupport, self).__init__()
  24. def __iter__(self):
  25. return self._registry.__iter__()
  26. @property
  27. def backends(self):
  28. return list(self._registry.values())
  29. @property
  30. def dirnames(self):
  31. return [backend.dirname for backend in self.backends]
  32. @property
  33. def all_schemes(self):
  34. schemes = []
  35. for backend in self.backends:
  36. schemes.extend(backend.schemes)
  37. return schemes
  38. def register(self, cls):
  39. if not hasattr(cls, 'name'):
  40. logger.warning('Cannot register VCS %s', cls.__name__)
  41. return
  42. if cls.name not in self._registry:
  43. self._registry[cls.name] = cls
  44. logger.debug('Registered VCS backend: %s', cls.name)
  45. def unregister(self, cls=None, name=None):
  46. if name in self._registry:
  47. del self._registry[name]
  48. elif cls in self._registry.values():
  49. del self._registry[cls.name]
  50. else:
  51. logger.warning('Cannot unregister because no class or name given')
  52. def get_backend_name(self, location):
  53. """
  54. Return the name of the version control backend if found at given
  55. location, e.g. vcs.get_backend_name('/path/to/vcs/checkout')
  56. """
  57. for vc_type in self._registry.values():
  58. logger.debug('Checking in %s for %s (%s)...',
  59. location, vc_type.dirname, vc_type.name)
  60. path = os.path.join(location, vc_type.dirname)
  61. if os.path.exists(path):
  62. logger.debug('Determine that %s uses VCS: %s',
  63. location, vc_type.name)
  64. return vc_type.name
  65. return None
  66. def get_backend(self, name):
  67. name = name.lower()
  68. if name in self._registry:
  69. return self._registry[name]
  70. def get_backend_from_location(self, location):
  71. vc_type = self.get_backend_name(location)
  72. if vc_type:
  73. return self.get_backend(vc_type)
  74. return None
  75. vcs = VcsSupport()
  76. class VersionControl(object):
  77. name = ''
  78. dirname = ''
  79. # List of supported schemes for this Version Control
  80. schemes = ()
  81. def __init__(self, url=None, *args, **kwargs):
  82. self.url = url
  83. super(VersionControl, self).__init__(*args, **kwargs)
  84. def _is_local_repository(self, repo):
  85. """
  86. posix absolute paths start with os.path.sep,
  87. win32 ones ones start with drive (like c:\\folder)
  88. """
  89. drive, tail = os.path.splitdrive(repo)
  90. return repo.startswith(os.path.sep) or drive
  91. # See issue #1083 for why this method was introduced:
  92. # https://github.com/pypa/pip/issues/1083
  93. def translate_egg_surname(self, surname):
  94. # For example, Django has branches of the form "stable/1.7.x".
  95. return surname.replace('/', '_')
  96. def export(self, location):
  97. """
  98. Export the repository at the url to the destination location
  99. i.e. only download the files, without vcs informations
  100. """
  101. raise NotImplementedError
  102. def get_url_rev(self):
  103. """
  104. Returns the correct repository URL and revision by parsing the given
  105. repository URL
  106. """
  107. error_message = (
  108. "Sorry, '%s' is a malformed VCS url. "
  109. "The format is <vcs>+<protocol>://<url>, "
  110. "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp"
  111. )
  112. assert '+' in self.url, error_message % self.url
  113. url = self.url.split('+', 1)[1]
  114. scheme, netloc, path, query, frag = urllib_parse.urlsplit(url)
  115. rev = None
  116. if '@' in path:
  117. path, rev = path.rsplit('@', 1)
  118. url = urllib_parse.urlunsplit((scheme, netloc, path, query, ''))
  119. return url, rev
  120. def get_info(self, location):
  121. """
  122. Returns (url, revision), where both are strings
  123. """
  124. assert not location.rstrip('/').endswith(self.dirname), \
  125. 'Bad directory: %s' % location
  126. return self.get_url(location), self.get_revision(location)
  127. def normalize_url(self, url):
  128. """
  129. Normalize a URL for comparison by unquoting it and removing any
  130. trailing slash.
  131. """
  132. return urllib_parse.unquote(url).rstrip('/')
  133. def compare_urls(self, url1, url2):
  134. """
  135. Compare two repo URLs for identity, ignoring incidental differences.
  136. """
  137. return (self.normalize_url(url1) == self.normalize_url(url2))
  138. def obtain(self, dest):
  139. """
  140. Called when installing or updating an editable package, takes the
  141. source path of the checkout.
  142. """
  143. raise NotImplementedError
  144. def switch(self, dest, url, rev_options):
  145. """
  146. Switch the repo at ``dest`` to point to ``URL``.
  147. """
  148. raise NotImplementedError
  149. def update(self, dest, rev_options):
  150. """
  151. Update an already-existing repo to the given ``rev_options``.
  152. """
  153. raise NotImplementedError
  154. def check_destination(self, dest, url, rev_options, rev_display):
  155. """
  156. Prepare a location to receive a checkout/clone.
  157. Return True if the location is ready for (and requires) a
  158. checkout/clone, False otherwise.
  159. """
  160. checkout = True
  161. prompt = False
  162. if os.path.exists(dest):
  163. checkout = False
  164. if os.path.exists(os.path.join(dest, self.dirname)):
  165. existing_url = self.get_url(dest)
  166. if self.compare_urls(existing_url, url):
  167. logger.debug(
  168. '%s in %s exists, and has correct URL (%s)',
  169. self.repo_name.title(),
  170. display_path(dest),
  171. url,
  172. )
  173. logger.info(
  174. 'Updating %s %s%s',
  175. display_path(dest),
  176. self.repo_name,
  177. rev_display,
  178. )
  179. self.update(dest, rev_options)
  180. else:
  181. logger.warning(
  182. '%s %s in %s exists with URL %s',
  183. self.name,
  184. self.repo_name,
  185. display_path(dest),
  186. existing_url,
  187. )
  188. prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ',
  189. ('s', 'i', 'w', 'b'))
  190. else:
  191. logger.warning(
  192. 'Directory %s already exists, and is not a %s %s.',
  193. dest,
  194. self.name,
  195. self.repo_name,
  196. )
  197. prompt = ('(i)gnore, (w)ipe, (b)ackup ', ('i', 'w', 'b'))
  198. if prompt:
  199. logger.warning(
  200. 'The plan is to install the %s repository %s',
  201. self.name,
  202. url,
  203. )
  204. response = ask_path_exists('What to do? %s' % prompt[0],
  205. prompt[1])
  206. if response == 's':
  207. logger.info(
  208. 'Switching %s %s to %s%s',
  209. self.repo_name,
  210. display_path(dest),
  211. url,
  212. rev_display,
  213. )
  214. self.switch(dest, url, rev_options)
  215. elif response == 'i':
  216. # do nothing
  217. pass
  218. elif response == 'w':
  219. logger.warning('Deleting %s', display_path(dest))
  220. rmtree(dest)
  221. checkout = True
  222. elif response == 'b':
  223. dest_dir = backup_dir(dest)
  224. logger.warning(
  225. 'Backing up %s to %s', display_path(dest), dest_dir,
  226. )
  227. shutil.move(dest, dest_dir)
  228. checkout = True
  229. return checkout
  230. def unpack(self, location):
  231. """
  232. Clean up current location and download the url repository
  233. (and vcs infos) into location
  234. """
  235. if os.path.exists(location):
  236. rmtree(location)
  237. self.obtain(location)
  238. def get_src_requirement(self, dist, location, find_tags=False):
  239. """
  240. Return a string representing the requirement needed to
  241. redownload the files currently present in location, something
  242. like:
  243. {repository_url}@{revision}#egg={project_name}-{version_identifier}
  244. If find_tags is True, try to find a tag matching the revision
  245. """
  246. raise NotImplementedError
  247. def get_url(self, location):
  248. """
  249. Return the url used at location
  250. Used in get_info or check_destination
  251. """
  252. raise NotImplementedError
  253. def get_revision(self, location):
  254. """
  255. Return the current revision of the files at location
  256. Used in get_info
  257. """
  258. raise NotImplementedError
  259. def run_command(self, cmd, show_stdout=True, cwd=None,
  260. raise_on_returncode=True,
  261. command_level=logging.DEBUG, command_desc=None,
  262. extra_environ=None):
  263. """
  264. Run a VCS subcommand
  265. This is simply a wrapper around call_subprocess that adds the VCS
  266. command name, and checks that the VCS is available
  267. """
  268. cmd = [self.name] + cmd
  269. try:
  270. return call_subprocess(cmd, show_stdout, cwd,
  271. raise_on_returncode, command_level,
  272. command_desc, extra_environ)
  273. except OSError as e:
  274. # errno.ENOENT = no such file or directory
  275. # In other words, the VCS executable isn't available
  276. if e.errno == errno.ENOENT:
  277. raise BadCommand('Cannot find command %r' % self.name)
  278. else:
  279. raise # re-raise exception if a different error occured
  280. def get_src_requirement(dist, location, find_tags):
  281. version_control = vcs.get_backend_from_location(location)
  282. if version_control:
  283. try:
  284. return version_control().get_src_requirement(dist,
  285. location,
  286. find_tags)
  287. except BadCommand:
  288. logger.warning(
  289. 'cannot determine version of editable source in %s '
  290. '(%s command not found in path)',
  291. location,
  292. version_control.name,
  293. )
  294. return dist.as_requirement()
  295. logger.warning(
  296. 'cannot determine version of editable source in %s (is not SVN '
  297. 'checkout, Git clone, Mercurial clone or Bazaar branch)',
  298. location,
  299. )
  300. return dist.as_requirement()