__init__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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 _filter(self, line):
  85. return (logging.DEBUG, line)
  86. def _is_local_repository(self, repo):
  87. """
  88. posix absolute paths start with os.path.sep,
  89. win32 ones ones start with drive (like c:\\folder)
  90. """
  91. drive, tail = os.path.splitdrive(repo)
  92. return repo.startswith(os.path.sep) or drive
  93. # See issue #1083 for why this method was introduced:
  94. # https://github.com/pypa/pip/issues/1083
  95. def translate_egg_surname(self, surname):
  96. # For example, Django has branches of the form "stable/1.7.x".
  97. return surname.replace('/', '_')
  98. def export(self, location):
  99. """
  100. Export the repository at the url to the destination location
  101. i.e. only download the files, without vcs informations
  102. """
  103. raise NotImplementedError
  104. def get_url_rev(self):
  105. """
  106. Returns the correct repository URL and revision by parsing the given
  107. repository URL
  108. """
  109. error_message = (
  110. "Sorry, '%s' is a malformed VCS url. "
  111. "The format is <vcs>+<protocol>://<url>, "
  112. "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp"
  113. )
  114. assert '+' in self.url, error_message % self.url
  115. url = self.url.split('+', 1)[1]
  116. scheme, netloc, path, query, frag = urllib_parse.urlsplit(url)
  117. rev = None
  118. if '@' in path:
  119. path, rev = path.rsplit('@', 1)
  120. url = urllib_parse.urlunsplit((scheme, netloc, path, query, ''))
  121. return url, rev
  122. def get_info(self, location):
  123. """
  124. Returns (url, revision), where both are strings
  125. """
  126. assert not location.rstrip('/').endswith(self.dirname), \
  127. 'Bad directory: %s' % location
  128. return self.get_url(location), self.get_revision(location)
  129. def normalize_url(self, url):
  130. """
  131. Normalize a URL for comparison by unquoting it and removing any
  132. trailing slash.
  133. """
  134. return urllib_parse.unquote(url).rstrip('/')
  135. def compare_urls(self, url1, url2):
  136. """
  137. Compare two repo URLs for identity, ignoring incidental differences.
  138. """
  139. return (self.normalize_url(url1) == self.normalize_url(url2))
  140. def obtain(self, dest):
  141. """
  142. Called when installing or updating an editable package, takes the
  143. source path of the checkout.
  144. """
  145. raise NotImplementedError
  146. def switch(self, dest, url, rev_options):
  147. """
  148. Switch the repo at ``dest`` to point to ``URL``.
  149. """
  150. raise NotImplementedError
  151. def update(self, dest, rev_options):
  152. """
  153. Update an already-existing repo to the given ``rev_options``.
  154. """
  155. raise NotImplementedError
  156. def check_destination(self, dest, url, rev_options, rev_display):
  157. """
  158. Prepare a location to receive a checkout/clone.
  159. Return True if the location is ready for (and requires) a
  160. checkout/clone, False otherwise.
  161. """
  162. checkout = True
  163. prompt = False
  164. if os.path.exists(dest):
  165. checkout = False
  166. if os.path.exists(os.path.join(dest, self.dirname)):
  167. existing_url = self.get_url(dest)
  168. if self.compare_urls(existing_url, url):
  169. logger.debug(
  170. '%s in %s exists, and has correct URL (%s)',
  171. self.repo_name.title(),
  172. display_path(dest),
  173. url,
  174. )
  175. logger.info(
  176. 'Updating %s %s%s',
  177. display_path(dest),
  178. self.repo_name,
  179. rev_display,
  180. )
  181. self.update(dest, rev_options)
  182. else:
  183. logger.warning(
  184. '%s %s in %s exists with URL %s',
  185. self.name,
  186. self.repo_name,
  187. display_path(dest),
  188. existing_url,
  189. )
  190. prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ',
  191. ('s', 'i', 'w', 'b'))
  192. else:
  193. logger.warning(
  194. 'Directory %s already exists, and is not a %s %s.',
  195. dest,
  196. self.name,
  197. self.repo_name,
  198. )
  199. prompt = ('(i)gnore, (w)ipe, (b)ackup ', ('i', 'w', 'b'))
  200. if prompt:
  201. logger.warning(
  202. 'The plan is to install the %s repository %s',
  203. self.name,
  204. url,
  205. )
  206. response = ask_path_exists('What to do? %s' % prompt[0],
  207. prompt[1])
  208. if response == 's':
  209. logger.info(
  210. 'Switching %s %s to %s%s',
  211. self.repo_name,
  212. display_path(dest),
  213. url,
  214. rev_display,
  215. )
  216. self.switch(dest, url, rev_options)
  217. elif response == 'i':
  218. # do nothing
  219. pass
  220. elif response == 'w':
  221. logger.warning('Deleting %s', display_path(dest))
  222. rmtree(dest)
  223. checkout = True
  224. elif response == 'b':
  225. dest_dir = backup_dir(dest)
  226. logger.warning(
  227. 'Backing up %s to %s', display_path(dest), dest_dir,
  228. )
  229. shutil.move(dest, dest_dir)
  230. checkout = True
  231. return checkout
  232. def unpack(self, location):
  233. """
  234. Clean up current location and download the url repository
  235. (and vcs infos) into location
  236. """
  237. if os.path.exists(location):
  238. rmtree(location)
  239. self.obtain(location)
  240. def get_src_requirement(self, dist, location, find_tags=False):
  241. """
  242. Return a string representing the requirement needed to
  243. redownload the files currently present in location, something
  244. like:
  245. {repository_url}@{revision}#egg={project_name}-{version_identifier}
  246. If find_tags is True, try to find a tag matching the revision
  247. """
  248. raise NotImplementedError
  249. def get_url(self, location):
  250. """
  251. Return the url used at location
  252. Used in get_info or check_destination
  253. """
  254. raise NotImplementedError
  255. def get_revision(self, location):
  256. """
  257. Return the current revision of the files at location
  258. Used in get_info
  259. """
  260. raise NotImplementedError
  261. def run_command(self, cmd, show_stdout=True,
  262. filter_stdout=None, cwd=None,
  263. raise_on_returncode=True,
  264. command_level=logging.DEBUG, command_desc=None,
  265. extra_environ=None):
  266. """
  267. Run a VCS subcommand
  268. This is simply a wrapper around call_subprocess that adds the VCS
  269. command name, and checks that the VCS is available
  270. """
  271. cmd = [self.name] + cmd
  272. try:
  273. return call_subprocess(cmd, show_stdout, filter_stdout, cwd,
  274. raise_on_returncode, command_level,
  275. command_desc, extra_environ)
  276. except OSError as e:
  277. # errno.ENOENT = no such file or directory
  278. # In other words, the VCS executable isn't available
  279. if e.errno == errno.ENOENT:
  280. raise BadCommand('Cannot find command %r' % self.name)
  281. else:
  282. raise # re-raise exception if a different error occured
  283. def get_src_requirement(dist, location, find_tags):
  284. version_control = vcs.get_backend_from_location(location)
  285. if version_control:
  286. try:
  287. return version_control().get_src_requirement(dist,
  288. location,
  289. find_tags)
  290. except BadCommand:
  291. logger.warning(
  292. 'cannot determine version of editable source in %s '
  293. '(%s command not found in path)',
  294. location,
  295. version_control.name,
  296. )
  297. return dist.as_requirement()
  298. logger.warning(
  299. 'cannot determine version of editable source in %s (is not SVN '
  300. 'checkout, Git clone, Mercurial clone or Bazaar branch)',
  301. location,
  302. )
  303. return dist.as_requirement()