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.

__init__.py 15KB

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