__init__.py 12 KB

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