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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  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._internal.exceptions import BadCommand
  10. from pip._internal.utils.misc import (
  11. display_path, backup_dir, call_subprocess, rmtree, ask_path_exists,
  12. )
  13. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  14. if MYPY_CHECK_RUNNING:
  15. from typing import ( # noqa: F401
  16. Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type
  17. )
  18. from pip._internal.utils.ui import SpinnerInterface # noqa: F401
  19. AuthInfo = Tuple[Optional[str], Optional[str]]
  20. __all__ = ['vcs']
  21. logger = logging.getLogger(__name__)
  22. class RemoteNotFoundError(Exception):
  23. pass
  24. class RevOptions(object):
  25. """
  26. Encapsulates a VCS-specific revision to install, along with any VCS
  27. install options.
  28. Instances of this class should be treated as if immutable.
  29. """
  30. def __init__(self, vcs, rev=None, extra_args=None):
  31. # type: (VersionControl, Optional[str], Optional[List[str]]) -> None
  32. """
  33. Args:
  34. vcs: a VersionControl object.
  35. rev: the name of the revision to install.
  36. extra_args: a list of extra options.
  37. """
  38. if extra_args is None:
  39. extra_args = []
  40. self.extra_args = extra_args
  41. self.rev = rev
  42. self.vcs = vcs
  43. def __repr__(self):
  44. return '<RevOptions {}: rev={!r}>'.format(self.vcs.name, self.rev)
  45. @property
  46. def arg_rev(self):
  47. # type: () -> Optional[str]
  48. if self.rev is None:
  49. return self.vcs.default_arg_rev
  50. return self.rev
  51. def to_args(self):
  52. # type: () -> List[str]
  53. """
  54. Return the VCS-specific command arguments.
  55. """
  56. args = [] # type: List[str]
  57. rev = self.arg_rev
  58. if rev is not None:
  59. args += self.vcs.get_base_rev_args(rev)
  60. args += self.extra_args
  61. return args
  62. def to_display(self):
  63. # type: () -> str
  64. if not self.rev:
  65. return ''
  66. return ' (to revision {})'.format(self.rev)
  67. def make_new(self, rev):
  68. # type: (str) -> RevOptions
  69. """
  70. Make a copy of the current instance, but with a new rev.
  71. Args:
  72. rev: the name of the revision for the new object.
  73. """
  74. return self.vcs.make_rev_options(rev, extra_args=self.extra_args)
  75. class VcsSupport(object):
  76. _registry = {} # type: Dict[str, Type[VersionControl]]
  77. schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
  78. def __init__(self):
  79. # type: () -> None
  80. # Register more schemes with urlparse for various version control
  81. # systems
  82. urllib_parse.uses_netloc.extend(self.schemes)
  83. # Python >= 2.7.4, 3.3 doesn't have uses_fragment
  84. if getattr(urllib_parse, 'uses_fragment', None):
  85. urllib_parse.uses_fragment.extend(self.schemes)
  86. super(VcsSupport, self).__init__()
  87. def __iter__(self):
  88. return self._registry.__iter__()
  89. @property
  90. def backends(self):
  91. # type: () -> List[Type[VersionControl]]
  92. return list(self._registry.values())
  93. @property
  94. def dirnames(self):
  95. # type: () -> List[str]
  96. return [backend.dirname for backend in self.backends]
  97. @property
  98. def all_schemes(self):
  99. # type: () -> List[str]
  100. schemes = [] # type: List[str]
  101. for backend in self.backends:
  102. schemes.extend(backend.schemes)
  103. return schemes
  104. def register(self, cls):
  105. # type: (Type[VersionControl]) -> None
  106. if not hasattr(cls, 'name'):
  107. logger.warning('Cannot register VCS %s', cls.__name__)
  108. return
  109. if cls.name not in self._registry:
  110. self._registry[cls.name] = cls
  111. logger.debug('Registered VCS backend: %s', cls.name)
  112. def unregister(self, cls=None, name=None):
  113. # type: (Optional[Type[VersionControl]], Optional[str]) -> None
  114. if name in self._registry:
  115. del self._registry[name]
  116. elif cls in self._registry.values():
  117. del self._registry[cls.name]
  118. else:
  119. logger.warning('Cannot unregister because no class or name given')
  120. def get_backend_type(self, location):
  121. # type: (str) -> Optional[Type[VersionControl]]
  122. """
  123. Return the type of the version control backend if found at given
  124. location, e.g. vcs.get_backend_type('/path/to/vcs/checkout')
  125. """
  126. for vc_type in self._registry.values():
  127. if vc_type.controls_location(location):
  128. logger.debug('Determine that %s uses VCS: %s',
  129. location, vc_type.name)
  130. return vc_type
  131. return None
  132. def get_backend(self, name):
  133. # type: (str) -> Optional[Type[VersionControl]]
  134. name = name.lower()
  135. if name in self._registry:
  136. return self._registry[name]
  137. return None
  138. vcs = VcsSupport()
  139. class VersionControl(object):
  140. name = ''
  141. dirname = ''
  142. repo_name = ''
  143. # List of supported schemes for this Version Control
  144. schemes = () # type: Tuple[str, ...]
  145. # Iterable of environment variable names to pass to call_subprocess().
  146. unset_environ = () # type: Tuple[str, ...]
  147. default_arg_rev = None # type: Optional[str]
  148. def __init__(self, url=None, *args, **kwargs):
  149. self.url = url
  150. super(VersionControl, self).__init__(*args, **kwargs)
  151. def get_base_rev_args(self, rev):
  152. """
  153. Return the base revision arguments for a vcs command.
  154. Args:
  155. rev: the name of a revision to install. Cannot be None.
  156. """
  157. raise NotImplementedError
  158. def make_rev_options(self, rev=None, extra_args=None):
  159. # type: (Optional[str], Optional[List[str]]) -> RevOptions
  160. """
  161. Return a RevOptions object.
  162. Args:
  163. rev: the name of a revision to install.
  164. extra_args: a list of extra options.
  165. """
  166. return RevOptions(self, rev, extra_args=extra_args)
  167. @classmethod
  168. def _is_local_repository(cls, repo):
  169. # type: (str) -> bool
  170. """
  171. posix absolute paths start with os.path.sep,
  172. win32 ones start with drive (like c:\\folder)
  173. """
  174. drive, tail = os.path.splitdrive(repo)
  175. return repo.startswith(os.path.sep) or bool(drive)
  176. def export(self, location):
  177. """
  178. Export the repository at the url to the destination location
  179. i.e. only download the files, without vcs informations
  180. """
  181. raise NotImplementedError
  182. def get_netloc_and_auth(self, netloc, scheme):
  183. """
  184. Parse the repository URL's netloc, and return the new netloc to use
  185. along with auth information.
  186. Args:
  187. netloc: the original repository URL netloc.
  188. scheme: the repository URL's scheme without the vcs prefix.
  189. This is mainly for the Subversion class to override, so that auth
  190. information can be provided via the --username and --password options
  191. instead of through the URL. For other subclasses like Git without
  192. such an option, auth information must stay in the URL.
  193. Returns: (netloc, (username, password)).
  194. """
  195. return netloc, (None, None)
  196. def get_url_rev_and_auth(self, url):
  197. # type: (str) -> Tuple[str, Optional[str], AuthInfo]
  198. """
  199. Parse the repository URL to use, and return the URL, revision,
  200. and auth info to use.
  201. Returns: (url, rev, (username, password)).
  202. """
  203. scheme, netloc, path, query, frag = urllib_parse.urlsplit(url)
  204. if '+' not in scheme:
  205. raise ValueError(
  206. "Sorry, {!r} is a malformed VCS url. "
  207. "The format is <vcs>+<protocol>://<url>, "
  208. "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url)
  209. )
  210. # Remove the vcs prefix.
  211. scheme = scheme.split('+', 1)[1]
  212. netloc, user_pass = self.get_netloc_and_auth(netloc, scheme)
  213. rev = None
  214. if '@' in path:
  215. path, rev = path.rsplit('@', 1)
  216. url = urllib_parse.urlunsplit((scheme, netloc, path, query, ''))
  217. return url, rev, user_pass
  218. def make_rev_args(self, username, password):
  219. """
  220. Return the RevOptions "extra arguments" to use in obtain().
  221. """
  222. return []
  223. def get_url_rev_options(self, url):
  224. # type: (str) -> Tuple[str, RevOptions]
  225. """
  226. Return the URL and RevOptions object to use in obtain() and in
  227. some cases export(), as a tuple (url, rev_options).
  228. """
  229. url, rev, user_pass = self.get_url_rev_and_auth(url)
  230. username, password = user_pass
  231. extra_args = self.make_rev_args(username, password)
  232. rev_options = self.make_rev_options(rev, extra_args=extra_args)
  233. return url, rev_options
  234. def normalize_url(self, url):
  235. # type: (str) -> str
  236. """
  237. Normalize a URL for comparison by unquoting it and removing any
  238. trailing slash.
  239. """
  240. return urllib_parse.unquote(url).rstrip('/')
  241. def compare_urls(self, url1, url2):
  242. # type: (str, str) -> bool
  243. """
  244. Compare two repo URLs for identity, ignoring incidental differences.
  245. """
  246. return (self.normalize_url(url1) == self.normalize_url(url2))
  247. def fetch_new(self, dest, url, rev_options):
  248. """
  249. Fetch a revision from a repository, in the case that this is the
  250. first fetch from the repository.
  251. Args:
  252. dest: the directory to fetch the repository to.
  253. rev_options: a RevOptions object.
  254. """
  255. raise NotImplementedError
  256. def switch(self, dest, url, rev_options):
  257. """
  258. Switch the repo at ``dest`` to point to ``URL``.
  259. Args:
  260. rev_options: a RevOptions object.
  261. """
  262. raise NotImplementedError
  263. def update(self, dest, url, rev_options):
  264. """
  265. Update an already-existing repo to the given ``rev_options``.
  266. Args:
  267. rev_options: a RevOptions object.
  268. """
  269. raise NotImplementedError
  270. def is_commit_id_equal(self, dest, name):
  271. """
  272. Return whether the id of the current commit equals the given name.
  273. Args:
  274. dest: the repository directory.
  275. name: a string name.
  276. """
  277. raise NotImplementedError
  278. def obtain(self, dest):
  279. # type: (str) -> None
  280. """
  281. Install or update in editable mode the package represented by this
  282. VersionControl object.
  283. Args:
  284. dest: the repository directory in which to install or update.
  285. """
  286. url, rev_options = self.get_url_rev_options(self.url)
  287. if not os.path.exists(dest):
  288. self.fetch_new(dest, url, rev_options)
  289. return
  290. rev_display = rev_options.to_display()
  291. if self.is_repository_directory(dest):
  292. existing_url = self.get_remote_url(dest)
  293. if self.compare_urls(existing_url, url):
  294. logger.debug(
  295. '%s in %s exists, and has correct URL (%s)',
  296. self.repo_name.title(),
  297. display_path(dest),
  298. url,
  299. )
  300. if not self.is_commit_id_equal(dest, rev_options.rev):
  301. logger.info(
  302. 'Updating %s %s%s',
  303. display_path(dest),
  304. self.repo_name,
  305. rev_display,
  306. )
  307. self.update(dest, url, rev_options)
  308. else:
  309. logger.info('Skipping because already up-to-date.')
  310. return
  311. logger.warning(
  312. '%s %s in %s exists with URL %s',
  313. self.name,
  314. self.repo_name,
  315. display_path(dest),
  316. existing_url,
  317. )
  318. prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ',
  319. ('s', 'i', 'w', 'b'))
  320. else:
  321. logger.warning(
  322. 'Directory %s already exists, and is not a %s %s.',
  323. dest,
  324. self.name,
  325. self.repo_name,
  326. )
  327. # https://github.com/python/mypy/issues/1174
  328. prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore
  329. ('i', 'w', 'b'))
  330. logger.warning(
  331. 'The plan is to install the %s repository %s',
  332. self.name,
  333. url,
  334. )
  335. response = ask_path_exists('What to do? %s' % prompt[0], prompt[1])
  336. if response == 'a':
  337. sys.exit(-1)
  338. if response == 'w':
  339. logger.warning('Deleting %s', display_path(dest))
  340. rmtree(dest)
  341. self.fetch_new(dest, url, rev_options)
  342. return
  343. if response == 'b':
  344. dest_dir = backup_dir(dest)
  345. logger.warning(
  346. 'Backing up %s to %s', display_path(dest), dest_dir,
  347. )
  348. shutil.move(dest, dest_dir)
  349. self.fetch_new(dest, url, rev_options)
  350. return
  351. # Do nothing if the response is "i".
  352. if response == 's':
  353. logger.info(
  354. 'Switching %s %s to %s%s',
  355. self.repo_name,
  356. display_path(dest),
  357. url,
  358. rev_display,
  359. )
  360. self.switch(dest, url, rev_options)
  361. def unpack(self, location):
  362. # type: (str) -> None
  363. """
  364. Clean up current location and download the url repository
  365. (and vcs infos) into location
  366. """
  367. if os.path.exists(location):
  368. rmtree(location)
  369. self.obtain(location)
  370. @classmethod
  371. def get_src_requirement(cls, location, project_name):
  372. """
  373. Return a string representing the requirement needed to
  374. redownload the files currently present in location, something
  375. like:
  376. {repository_url}@{revision}#egg={project_name}-{version_identifier}
  377. """
  378. raise NotImplementedError
  379. @classmethod
  380. def get_remote_url(cls, location):
  381. """
  382. Return the url used at location
  383. Raises RemoteNotFoundError if the repository does not have a remote
  384. url configured.
  385. """
  386. raise NotImplementedError
  387. @classmethod
  388. def get_revision(cls, location):
  389. """
  390. Return the current commit id of the files at the given location.
  391. """
  392. raise NotImplementedError
  393. @classmethod
  394. def run_command(
  395. cls,
  396. cmd, # type: List[str]
  397. show_stdout=True, # type: bool
  398. cwd=None, # type: Optional[str]
  399. on_returncode='raise', # type: str
  400. extra_ok_returncodes=None, # type: Optional[Iterable[int]]
  401. command_desc=None, # type: Optional[str]
  402. extra_environ=None, # type: Optional[Mapping[str, Any]]
  403. spinner=None # type: Optional[SpinnerInterface]
  404. ):
  405. # type: (...) -> Optional[Text]
  406. """
  407. Run a VCS subcommand
  408. This is simply a wrapper around call_subprocess that adds the VCS
  409. command name, and checks that the VCS is available
  410. """
  411. cmd = [cls.name] + cmd
  412. try:
  413. return call_subprocess(cmd, show_stdout, cwd,
  414. on_returncode=on_returncode,
  415. extra_ok_returncodes=extra_ok_returncodes,
  416. command_desc=command_desc,
  417. extra_environ=extra_environ,
  418. unset_environ=cls.unset_environ,
  419. spinner=spinner)
  420. except OSError as e:
  421. # errno.ENOENT = no such file or directory
  422. # In other words, the VCS executable isn't available
  423. if e.errno == errno.ENOENT:
  424. raise BadCommand(
  425. 'Cannot find command %r - do you have '
  426. '%r installed and in your '
  427. 'PATH?' % (cls.name, cls.name))
  428. else:
  429. raise # re-raise exception if a different error occurred
  430. @classmethod
  431. def is_repository_directory(cls, path):
  432. # type: (str) -> bool
  433. """
  434. Return whether a directory path is a repository directory.
  435. """
  436. logger.debug('Checking in %s for %s (%s)...',
  437. path, cls.dirname, cls.name)
  438. return os.path.exists(os.path.join(path, cls.dirname))
  439. @classmethod
  440. def controls_location(cls, location):
  441. # type: (str) -> bool
  442. """
  443. Check if a location is controlled by the vcs.
  444. It is meant to be overridden to implement smarter detection
  445. mechanisms for specific vcs.
  446. This can do more than is_repository_directory() alone. For example,
  447. the Git override checks that Git is actually available.
  448. """
  449. return cls.is_repository_directory(location)