图片解析应用
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.

324 lines
12 KiB

  1. import logging
  2. import os
  3. import re
  4. from typing import List, Optional, Tuple
  5. from pip._internal.utils.misc import (
  6. HiddenText,
  7. display_path,
  8. is_console_interactive,
  9. is_installable_dir,
  10. split_auth_from_netloc,
  11. )
  12. from pip._internal.utils.subprocess import CommandArgs, make_command
  13. from pip._internal.vcs.versioncontrol import (
  14. AuthInfo,
  15. RemoteNotFoundError,
  16. RevOptions,
  17. VersionControl,
  18. vcs,
  19. )
  20. logger = logging.getLogger(__name__)
  21. _svn_xml_url_re = re.compile('url="([^"]+)"')
  22. _svn_rev_re = re.compile(r'committed-rev="(\d+)"')
  23. _svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"')
  24. _svn_info_xml_url_re = re.compile(r"<url>(.*)</url>")
  25. class Subversion(VersionControl):
  26. name = "svn"
  27. dirname = ".svn"
  28. repo_name = "checkout"
  29. schemes = ("svn+ssh", "svn+http", "svn+https", "svn+svn", "svn+file")
  30. @classmethod
  31. def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:
  32. return True
  33. @staticmethod
  34. def get_base_rev_args(rev: str) -> List[str]:
  35. return ["-r", rev]
  36. @classmethod
  37. def get_revision(cls, location: str) -> str:
  38. """
  39. Return the maximum revision for all files under a given location
  40. """
  41. # Note: taken from setuptools.command.egg_info
  42. revision = 0
  43. for base, dirs, _ in os.walk(location):
  44. if cls.dirname not in dirs:
  45. dirs[:] = []
  46. continue # no sense walking uncontrolled subdirs
  47. dirs.remove(cls.dirname)
  48. entries_fn = os.path.join(base, cls.dirname, "entries")
  49. if not os.path.exists(entries_fn):
  50. # FIXME: should we warn?
  51. continue
  52. dirurl, localrev = cls._get_svn_url_rev(base)
  53. if base == location:
  54. assert dirurl is not None
  55. base = dirurl + "/" # save the root url
  56. elif not dirurl or not dirurl.startswith(base):
  57. dirs[:] = []
  58. continue # not part of the same svn tree, skip it
  59. revision = max(revision, localrev)
  60. return str(revision)
  61. @classmethod
  62. def get_netloc_and_auth(
  63. cls, netloc: str, scheme: str
  64. ) -> Tuple[str, Tuple[Optional[str], Optional[str]]]:
  65. """
  66. This override allows the auth information to be passed to svn via the
  67. --username and --password options instead of via the URL.
  68. """
  69. if scheme == "ssh":
  70. # The --username and --password options can't be used for
  71. # svn+ssh URLs, so keep the auth information in the URL.
  72. return super().get_netloc_and_auth(netloc, scheme)
  73. return split_auth_from_netloc(netloc)
  74. @classmethod
  75. def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
  76. # hotfix the URL scheme after removing svn+ from svn+ssh:// re-add it
  77. url, rev, user_pass = super().get_url_rev_and_auth(url)
  78. if url.startswith("ssh://"):
  79. url = "svn+" + url
  80. return url, rev, user_pass
  81. @staticmethod
  82. def make_rev_args(
  83. username: Optional[str], password: Optional[HiddenText]
  84. ) -> CommandArgs:
  85. extra_args: CommandArgs = []
  86. if username:
  87. extra_args += ["--username", username]
  88. if password:
  89. extra_args += ["--password", password]
  90. return extra_args
  91. @classmethod
  92. def get_remote_url(cls, location: str) -> str:
  93. # In cases where the source is in a subdirectory, we have to look up in
  94. # the location until we find a valid project root.
  95. orig_location = location
  96. while not is_installable_dir(location):
  97. last_location = location
  98. location = os.path.dirname(location)
  99. if location == last_location:
  100. # We've traversed up to the root of the filesystem without
  101. # finding a Python project.
  102. logger.warning(
  103. "Could not find Python project for directory %s (tried all "
  104. "parent directories)",
  105. orig_location,
  106. )
  107. raise RemoteNotFoundError
  108. url, _rev = cls._get_svn_url_rev(location)
  109. if url is None:
  110. raise RemoteNotFoundError
  111. return url
  112. @classmethod
  113. def _get_svn_url_rev(cls, location: str) -> Tuple[Optional[str], int]:
  114. from pip._internal.exceptions import InstallationError
  115. entries_path = os.path.join(location, cls.dirname, "entries")
  116. if os.path.exists(entries_path):
  117. with open(entries_path) as f:
  118. data = f.read()
  119. else: # subversion >= 1.7 does not have the 'entries' file
  120. data = ""
  121. url = None
  122. if data.startswith("8") or data.startswith("9") or data.startswith("10"):
  123. entries = list(map(str.splitlines, data.split("\n\x0c\n")))
  124. del entries[0][0] # get rid of the '8'
  125. url = entries[0][3]
  126. revs = [int(d[9]) for d in entries if len(d) > 9 and d[9]] + [0]
  127. elif data.startswith("<?xml"):
  128. match = _svn_xml_url_re.search(data)
  129. if not match:
  130. raise ValueError(f"Badly formatted data: {data!r}")
  131. url = match.group(1) # get repository URL
  132. revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0]
  133. else:
  134. try:
  135. # subversion >= 1.7
  136. # Note that using get_remote_call_options is not necessary here
  137. # because `svn info` is being run against a local directory.
  138. # We don't need to worry about making sure interactive mode
  139. # is being used to prompt for passwords, because passwords
  140. # are only potentially needed for remote server requests.
  141. xml = cls.run_command(
  142. ["info", "--xml", location],
  143. show_stdout=False,
  144. stdout_only=True,
  145. )
  146. match = _svn_info_xml_url_re.search(xml)
  147. assert match is not None
  148. url = match.group(1)
  149. revs = [int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)]
  150. except InstallationError:
  151. url, revs = None, []
  152. if revs:
  153. rev = max(revs)
  154. else:
  155. rev = 0
  156. return url, rev
  157. @classmethod
  158. def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
  159. """Always assume the versions don't match"""
  160. return False
  161. def __init__(self, use_interactive: Optional[bool] = None) -> None:
  162. if use_interactive is None:
  163. use_interactive = is_console_interactive()
  164. self.use_interactive = use_interactive
  165. # This member is used to cache the fetched version of the current
  166. # ``svn`` client.
  167. # Special value definitions:
  168. # None: Not evaluated yet.
  169. # Empty tuple: Could not parse version.
  170. self._vcs_version: Optional[Tuple[int, ...]] = None
  171. super().__init__()
  172. def call_vcs_version(self) -> Tuple[int, ...]:
  173. """Query the version of the currently installed Subversion client.
  174. :return: A tuple containing the parts of the version information or
  175. ``()`` if the version returned from ``svn`` could not be parsed.
  176. :raises: BadCommand: If ``svn`` is not installed.
  177. """
  178. # Example versions:
  179. # svn, version 1.10.3 (r1842928)
  180. # compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0
  181. # svn, version 1.7.14 (r1542130)
  182. # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu
  183. # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)
  184. # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2
  185. version_prefix = "svn, version "
  186. version = self.run_command(["--version"], show_stdout=False, stdout_only=True)
  187. if not version.startswith(version_prefix):
  188. return ()
  189. version = version[len(version_prefix) :].split()[0]
  190. version_list = version.partition("-")[0].split(".")
  191. try:
  192. parsed_version = tuple(map(int, version_list))
  193. except ValueError:
  194. return ()
  195. return parsed_version
  196. def get_vcs_version(self) -> Tuple[int, ...]:
  197. """Return the version of the currently installed Subversion client.
  198. If the version of the Subversion client has already been queried,
  199. a cached value will be used.
  200. :return: A tuple containing the parts of the version information or
  201. ``()`` if the version returned from ``svn`` could not be parsed.
  202. :raises: BadCommand: If ``svn`` is not installed.
  203. """
  204. if self._vcs_version is not None:
  205. # Use cached version, if available.
  206. # If parsing the version failed previously (empty tuple),
  207. # do not attempt to parse it again.
  208. return self._vcs_version
  209. vcs_version = self.call_vcs_version()
  210. self._vcs_version = vcs_version
  211. return vcs_version
  212. def get_remote_call_options(self) -> CommandArgs:
  213. """Return options to be used on calls to Subversion that contact the server.
  214. These options are applicable for the following ``svn`` subcommands used
  215. in this class.
  216. - checkout
  217. - switch
  218. - update
  219. :return: A list of command line arguments to pass to ``svn``.
  220. """
  221. if not self.use_interactive:
  222. # --non-interactive switch is available since Subversion 0.14.4.
  223. # Subversion < 1.8 runs in interactive mode by default.
  224. return ["--non-interactive"]
  225. svn_version = self.get_vcs_version()
  226. # By default, Subversion >= 1.8 runs in non-interactive mode if
  227. # stdin is not a TTY. Since that is how pip invokes SVN, in
  228. # call_subprocess(), pip must pass --force-interactive to ensure
  229. # the user can be prompted for a password, if required.
  230. # SVN added the --force-interactive option in SVN 1.8. Since
  231. # e.g. RHEL/CentOS 7, which is supported until 2024, ships with
  232. # SVN 1.7, pip should continue to support SVN 1.7. Therefore, pip
  233. # can't safely add the option if the SVN version is < 1.8 (or unknown).
  234. if svn_version >= (1, 8):
  235. return ["--force-interactive"]
  236. return []
  237. def fetch_new(
  238. self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
  239. ) -> None:
  240. rev_display = rev_options.to_display()
  241. logger.info(
  242. "Checking out %s%s to %s",
  243. url,
  244. rev_display,
  245. display_path(dest),
  246. )
  247. if verbosity <= 0:
  248. flag = "--quiet"
  249. else:
  250. flag = ""
  251. cmd_args = make_command(
  252. "checkout",
  253. flag,
  254. self.get_remote_call_options(),
  255. rev_options.to_args(),
  256. url,
  257. dest,
  258. )
  259. self.run_command(cmd_args)
  260. def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
  261. cmd_args = make_command(
  262. "switch",
  263. self.get_remote_call_options(),
  264. rev_options.to_args(),
  265. url,
  266. dest,
  267. )
  268. self.run_command(cmd_args)
  269. def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
  270. cmd_args = make_command(
  271. "update",
  272. self.get_remote_call_options(),
  273. rev_options.to_args(),
  274. dest,
  275. )
  276. self.run_command(cmd_args)
  277. vcs.register(Subversion)