Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/vcs/subversion.py
4804 views
import logging1import os2import re3from typing import List, Optional, Tuple45from pip._internal.utils.misc import (6HiddenText,7display_path,8is_console_interactive,9is_installable_dir,10split_auth_from_netloc,11)12from pip._internal.utils.subprocess import CommandArgs, make_command13from pip._internal.vcs.versioncontrol import (14AuthInfo,15RemoteNotFoundError,16RevOptions,17VersionControl,18vcs,19)2021logger = logging.getLogger(__name__)2223_svn_xml_url_re = re.compile('url="([^"]+)"')24_svn_rev_re = re.compile(r'committed-rev="(\d+)"')25_svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"')26_svn_info_xml_url_re = re.compile(r"<url>(.*)</url>")272829class Subversion(VersionControl):30name = "svn"31dirname = ".svn"32repo_name = "checkout"33schemes = ("svn+ssh", "svn+http", "svn+https", "svn+svn", "svn+file")3435@classmethod36def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:37return True3839@staticmethod40def get_base_rev_args(rev: str) -> List[str]:41return ["-r", rev]4243@classmethod44def get_revision(cls, location: str) -> str:45"""46Return the maximum revision for all files under a given location47"""48# Note: taken from setuptools.command.egg_info49revision = 05051for base, dirs, _ in os.walk(location):52if cls.dirname not in dirs:53dirs[:] = []54continue # no sense walking uncontrolled subdirs55dirs.remove(cls.dirname)56entries_fn = os.path.join(base, cls.dirname, "entries")57if not os.path.exists(entries_fn):58# FIXME: should we warn?59continue6061dirurl, localrev = cls._get_svn_url_rev(base)6263if base == location:64assert dirurl is not None65base = dirurl + "/" # save the root url66elif not dirurl or not dirurl.startswith(base):67dirs[:] = []68continue # not part of the same svn tree, skip it69revision = max(revision, localrev)70return str(revision)7172@classmethod73def get_netloc_and_auth(74cls, netloc: str, scheme: str75) -> Tuple[str, Tuple[Optional[str], Optional[str]]]:76"""77This override allows the auth information to be passed to svn via the78--username and --password options instead of via the URL.79"""80if scheme == "ssh":81# The --username and --password options can't be used for82# svn+ssh URLs, so keep the auth information in the URL.83return super().get_netloc_and_auth(netloc, scheme)8485return split_auth_from_netloc(netloc)8687@classmethod88def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:89# hotfix the URL scheme after removing svn+ from svn+ssh:// readd it90url, rev, user_pass = super().get_url_rev_and_auth(url)91if url.startswith("ssh://"):92url = "svn+" + url93return url, rev, user_pass9495@staticmethod96def make_rev_args(97username: Optional[str], password: Optional[HiddenText]98) -> CommandArgs:99extra_args: CommandArgs = []100if username:101extra_args += ["--username", username]102if password:103extra_args += ["--password", password]104105return extra_args106107@classmethod108def get_remote_url(cls, location: str) -> str:109# In cases where the source is in a subdirectory, we have to look up in110# the location until we find a valid project root.111orig_location = location112while not is_installable_dir(location):113last_location = location114location = os.path.dirname(location)115if location == last_location:116# We've traversed up to the root of the filesystem without117# finding a Python project.118logger.warning(119"Could not find Python project for directory %s (tried all "120"parent directories)",121orig_location,122)123raise RemoteNotFoundError124125url, _rev = cls._get_svn_url_rev(location)126if url is None:127raise RemoteNotFoundError128129return url130131@classmethod132def _get_svn_url_rev(cls, location: str) -> Tuple[Optional[str], int]:133from pip._internal.exceptions import InstallationError134135entries_path = os.path.join(location, cls.dirname, "entries")136if os.path.exists(entries_path):137with open(entries_path) as f:138data = f.read()139else: # subversion >= 1.7 does not have the 'entries' file140data = ""141142url = None143if data.startswith("8") or data.startswith("9") or data.startswith("10"):144entries = list(map(str.splitlines, data.split("\n\x0c\n")))145del entries[0][0] # get rid of the '8'146url = entries[0][3]147revs = [int(d[9]) for d in entries if len(d) > 9 and d[9]] + [0]148elif data.startswith("<?xml"):149match = _svn_xml_url_re.search(data)150if not match:151raise ValueError(f"Badly formatted data: {data!r}")152url = match.group(1) # get repository URL153revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0]154else:155try:156# subversion >= 1.7157# Note that using get_remote_call_options is not necessary here158# because `svn info` is being run against a local directory.159# We don't need to worry about making sure interactive mode160# is being used to prompt for passwords, because passwords161# are only potentially needed for remote server requests.162xml = cls.run_command(163["info", "--xml", location],164show_stdout=False,165stdout_only=True,166)167match = _svn_info_xml_url_re.search(xml)168assert match is not None169url = match.group(1)170revs = [int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)]171except InstallationError:172url, revs = None, []173174if revs:175rev = max(revs)176else:177rev = 0178179return url, rev180181@classmethod182def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:183"""Always assume the versions don't match"""184return False185186def __init__(self, use_interactive: bool = None) -> None:187if use_interactive is None:188use_interactive = is_console_interactive()189self.use_interactive = use_interactive190191# This member is used to cache the fetched version of the current192# ``svn`` client.193# Special value definitions:194# None: Not evaluated yet.195# Empty tuple: Could not parse version.196self._vcs_version: Optional[Tuple[int, ...]] = None197198super().__init__()199200def call_vcs_version(self) -> Tuple[int, ...]:201"""Query the version of the currently installed Subversion client.202203:return: A tuple containing the parts of the version information or204``()`` if the version returned from ``svn`` could not be parsed.205:raises: BadCommand: If ``svn`` is not installed.206"""207# Example versions:208# svn, version 1.10.3 (r1842928)209# compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0210# svn, version 1.7.14 (r1542130)211# compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu212# svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)213# compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2214version_prefix = "svn, version "215version = self.run_command(["--version"], show_stdout=False, stdout_only=True)216if not version.startswith(version_prefix):217return ()218219version = version[len(version_prefix) :].split()[0]220version_list = version.partition("-")[0].split(".")221try:222parsed_version = tuple(map(int, version_list))223except ValueError:224return ()225226return parsed_version227228def get_vcs_version(self) -> Tuple[int, ...]:229"""Return the version of the currently installed Subversion client.230231If the version of the Subversion client has already been queried,232a cached value will be used.233234:return: A tuple containing the parts of the version information or235``()`` if the version returned from ``svn`` could not be parsed.236:raises: BadCommand: If ``svn`` is not installed.237"""238if self._vcs_version is not None:239# Use cached version, if available.240# If parsing the version failed previously (empty tuple),241# do not attempt to parse it again.242return self._vcs_version243244vcs_version = self.call_vcs_version()245self._vcs_version = vcs_version246return vcs_version247248def get_remote_call_options(self) -> CommandArgs:249"""Return options to be used on calls to Subversion that contact the server.250251These options are applicable for the following ``svn`` subcommands used252in this class.253254- checkout255- switch256- update257258:return: A list of command line arguments to pass to ``svn``.259"""260if not self.use_interactive:261# --non-interactive switch is available since Subversion 0.14.4.262# Subversion < 1.8 runs in interactive mode by default.263return ["--non-interactive"]264265svn_version = self.get_vcs_version()266# By default, Subversion >= 1.8 runs in non-interactive mode if267# stdin is not a TTY. Since that is how pip invokes SVN, in268# call_subprocess(), pip must pass --force-interactive to ensure269# the user can be prompted for a password, if required.270# SVN added the --force-interactive option in SVN 1.8. Since271# e.g. RHEL/CentOS 7, which is supported until 2024, ships with272# SVN 1.7, pip should continue to support SVN 1.7. Therefore, pip273# can't safely add the option if the SVN version is < 1.8 (or unknown).274if svn_version >= (1, 8):275return ["--force-interactive"]276277return []278279def fetch_new(280self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int281) -> None:282rev_display = rev_options.to_display()283logger.info(284"Checking out %s%s to %s",285url,286rev_display,287display_path(dest),288)289if verbosity <= 0:290flag = "--quiet"291else:292flag = ""293cmd_args = make_command(294"checkout",295flag,296self.get_remote_call_options(),297rev_options.to_args(),298url,299dest,300)301self.run_command(cmd_args)302303def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:304cmd_args = make_command(305"switch",306self.get_remote_call_options(),307rev_options.to_args(),308url,309dest,310)311self.run_command(cmd_args)312313def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:314cmd_args = make_command(315"update",316self.get_remote_call_options(),317rev_options.to_args(),318dest,319)320self.run_command(cmd_args)321322323vcs.register(Subversion)324325326