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.

versioncontrol.py 21KB

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