
管理员
站长
- UID
- 1
- 积分
- 224315
- 余额
- 13 R
- Moe币
- 84800
- 在线时间
- 264 小时
- 注册时间
- 2025-12-28
- 最后登录
- 2026-9-19
|
"""Handles all VCS (version control) support"""
import logging
import os
import shutil
import sys
import urllib.parse
from dataclasses import dataclass, field
from typing import (
Any,
Dict,
Iterable,
Iterator,
List,
Literal,
Mapping,
Optional,
Tuple,
Type,
Union,
)
from pip._internal.cli.spinners import SpinnerInterface
from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.utils.misc import (
HiddenText,
ask_path_exists,
backup_dir,
display_path,
hide_url,
hide_value,
is_installable_dir,
rmtree,
)
from pip._internal.utils.subprocess import (
CommandArgs,
call_subprocess,
format_command_args,
make_command,
)
__all__ = ["vcs"]
logger = logging.getLogger(__name__)
AuthInfo = Tuple[Optional[str], Optional[str]]
def is_url(name: str) -> bool:
"""
Return true if the name looks like a URL.
"""
scheme = urllib.parse.urlsplit(name).scheme
if not scheme:
return False
return scheme in ["http", "https", "file", "ftp"] + vcs.all_schemes
def make_vcs_requirement_url(
repo_url: str, rev: str, project_name: str, subdir: Optional[str] = None
) -> str:
"""
Return the URL for a VCS requirement.
Args:
repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+").
project_name: the (unescaped) project name.
"""
egg_project_name = project_name.replace("-", "_")
req = f"{repo_url}@{rev}#egg={egg_project_name}"
if subdir:
req += f"&subdirectory={subdir}"
return req
def find_path_to_project_root_from_repo_root(
location: str, repo_root: str
) -> Optional[str]:
"""
Find the the Python project's root by searching up the filesystem from
`location`. Return the path to project root relative to `repo_root`.
Return None if the project root is `repo_root`, or cannot be found.
"""
# find project root.
orig_location = location
while not is_installable_dir(location):
last_location = location
location = os.path.dirname(location)
if location == last_location:
# We've traversed up to the root of the filesystem without
# finding a Python project.
logger.warning(
"Could not find a Python project for directory %s (tried all "
"parent directories)",
orig_location,
)
return None
if os.path.samefile(repo_root, location):
return None
return os.path.relpath(location, repo_root)
class RemoteNotFoundError(Exception):
pass
class RemoteNotValidError(Exception):
def __init__(self, url: str):
super().__init__(url)
self.url = url
@dataclass(frozen=True)
class RevOptions:
"""
Encapsulates a VCS-specific revision to install, along with any VCS
install options.
Args:
vc_class: a VersionControl subclass.
rev: the name of the revision to install.
extra_args: a list of extra options.
"""
vc_class: Type["VersionControl"]
rev: Optional[str] = None
extra_args: CommandArgs = field(default_factory=list)
branch_name: Optional[str] = None
def __repr__(self) -> str:
return f"<RevOptions {self.vc_class.name}: rev={self.rev!r}>"
@property
def arg_rev(self) -> Optional[str]:
if self.rev is None:
return self.vc_class.default_arg_rev
return self.rev
def to_args(self) -> CommandArgs:
"""
Return the VCS-specific command arguments.
"""
args: CommandArgs = []
rev = self.arg_rev
if rev is not None:
args += self.vc_class.get_base_rev_args(rev)
args += self.extra_args
return args
def to_display(self) -> str:
if not self.rev:
return ""
return f" (to revision {self.rev})"
def make_new(self, rev: str) -> "RevOptions":
"""
Make a copy of the current instance, but with a new rev.
Args:
rev: the name of the revision for the new object.
"""
return self.vc_class.make_rev_options(rev, extra_args=self.extra_args)
class VcsSupport:
_registry: Dict[str, "VersionControl"] = {}
schemes = ["ssh", "git", "hg", "bzr", "sftp", "svn"]
def __init__(self) -> None:
# Register more schemes with urlparse for various version control
# systems
urllib.parse.uses_netloc.extend(self.schemes)
super().__init__()
def __iter__(self) -> Iterator[str]:
return self._registry.__iter__()
@property
def backends(self) -> List["VersionControl"]:
return list(self._registry.values())
@property
def dirnames(self) -> List[str]:
return [backend.dirname for backend in self.backends]
@property
def all_schemes(self) -> List[str]:
schemes: List[str] = []
for backend in self.backends:
schemes.extend(backend.schemes)
return schemes
def register(self, cls: Type["VersionControl"]) -> None:
if not hasattr(cls, "name"):
logger.warning("Cannot register VCS %s", cls.__name__)
return
if cls.name not in self._registry:
self._registry[cls.name] = cls()
logger.debug("Registered VCS backend: %s", cls.name)
def unregister(self, name: str) -> None:
if name in self._registry:
del self._registry[name]
def get_backend_for_dir(self, location: str) -> Optional["VersionControl"]:
"""
Return a VersionControl object if a repository of that type is found
at the given directory.
"""
vcs_backends = {}
for vcs_backend in self._registry.values():
repo_path = vcs_backend.get_repository_root(location)
if not repo_path:
continue
logger.debug("Determine that %s uses VCS: %s", location, vcs_backend.name)
vcs_backends[repo_path] = vcs_backend
if not vcs_backends:
return None
# Choose the VCS in the inner-most directory. Since all repository
# roots found here would be either `location` or one of its
# parents, the longest path should have the most path components,
# i.e. the backend representing the inner-most repository.
inner_most_repo_path = max(vcs_backends, key=len)
return vcs_backends[inner_most_repo_path]
def get_backend_for_scheme(self, scheme: str) -> Optional["VersionControl"]:
"""
Return a VersionControl object or None.
"""
for vcs_backend in self._registry.values():
if scheme in vcs_backend.schemes:
return vcs_backend
return None
def get_backend(self, name: str) -> Optional["VersionControl"]:
"""
Return a VersionControl object or None.
"""
name = name.lower()
return self._registry.get(name)
vcs = VcsSupport()
class VersionControl:
name = ""
dirname = ""
repo_name = ""
# List of supported schemes for this Version Control
schemes: Tuple[str, ...] = ()
# Iterable of environment variable names to pass to call_subprocess().
unset_environ: Tuple[str, ...] = ()
default_arg_rev: Optional[str] = None
@classmethod
def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:
"""
Return whether the vcs prefix (e.g. "git+") should be added to a
repository's remote url when used in a requirement.
"""
return not remote_url.lower().startswith(f"{cls.name}:")
@classmethod
def get_subdirectory(cls, location: str) -> Optional[str]:
"""
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
"""
return None
@classmethod
def get_requirement_revision(cls, repo_dir: str) -> str:
"""
Return the revision string that should be used in a requirement.
"""
return cls.get_revision(repo_dir)
@classmethod
def get_src_requirement(cls, repo_dir: str, project_name: str) -> str:
"""
Return the requirement string to use to redownload the files
currently at the given repository directory.
Args:
project_name: the (unescaped) project name.
The return value has a form similar to the following:
{repository_url}@{revision}#egg={project_name}
"""
repo_url = cls.get_remote_url(repo_dir)
if cls.should_add_vcs_url_prefix(repo_url):
repo_url = f"{cls.name}+{repo_url}"
revision = cls.get_requirement_revision(repo_dir)
subdir = cls.get_subdirectory(repo_dir)
req = make_vcs_requirement_url(repo_url, revision, project_name, subdir=subdir)
return req
@staticmethod
def get_base_rev_args(rev: str) -> List[str]:
"""
Return the base revision arguments for a vcs command.
Args:
rev: the name |
|