Path: blob/master/venv/Lib/site-packages/pip/_internal/vcs/git.py
811 views
# The following comment should be removed at some point in the future.1# mypy: disallow-untyped-defs=False23from __future__ import absolute_import45import logging6import os.path7import re89from pip._vendor.packaging.version import parse as parse_version10from pip._vendor.six.moves.urllib import parse as urllib_parse11from pip._vendor.six.moves.urllib import request as urllib_request1213from pip._internal.exceptions import BadCommand, InstallationError14from pip._internal.utils.misc import display_path, hide_url15from pip._internal.utils.subprocess import make_command16from pip._internal.utils.temp_dir import TempDirectory17from pip._internal.utils.typing import MYPY_CHECK_RUNNING18from pip._internal.vcs.versioncontrol import (19RemoteNotFoundError,20VersionControl,21find_path_to_setup_from_repo_root,22vcs,23)2425if MYPY_CHECK_RUNNING:26from typing import Optional, Tuple27from pip._internal.utils.misc import HiddenText28from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions293031urlsplit = urllib_parse.urlsplit32urlunsplit = urllib_parse.urlunsplit333435logger = logging.getLogger(__name__)363738HASH_REGEX = re.compile('^[a-fA-F0-9]{40}$')394041def looks_like_hash(sha):42return bool(HASH_REGEX.match(sha))434445class Git(VersionControl):46name = 'git'47dirname = '.git'48repo_name = 'clone'49schemes = (50'git', 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file',51)52# Prevent the user's environment variables from interfering with pip:53# https://github.com/pypa/pip/issues/113054unset_environ = ('GIT_DIR', 'GIT_WORK_TREE')55default_arg_rev = 'HEAD'5657@staticmethod58def get_base_rev_args(rev):59return [rev]6061def is_immutable_rev_checkout(self, url, dest):62# type: (str, str) -> bool63_, rev_options = self.get_url_rev_options(hide_url(url))64if not rev_options.rev:65return False66if not self.is_commit_id_equal(dest, rev_options.rev):67# the current commit is different from rev,68# which means rev was something else than a commit hash69return False70# return False in the rare case rev is both a commit hash71# and a tag or a branch; we don't want to cache in that case72# because that branch/tag could point to something else in the future73is_tag_or_branch = bool(74self.get_revision_sha(dest, rev_options.rev)[0]75)76return not is_tag_or_branch7778def get_git_version(self):79VERSION_PFX = 'git version '80version = self.run_command(['version'], show_stdout=False)81if version.startswith(VERSION_PFX):82version = version[len(VERSION_PFX):].split()[0]83else:84version = ''85# get first 3 positions of the git version because86# on windows it is x.y.z.windows.t, and this parses as87# LegacyVersion which always smaller than a Version.88version = '.'.join(version.split('.')[:3])89return parse_version(version)9091@classmethod92def get_current_branch(cls, location):93"""94Return the current branch, or None if HEAD isn't at a branch95(e.g. detached HEAD).96"""97# git-symbolic-ref exits with empty stdout if "HEAD" is a detached98# HEAD rather than a symbolic ref. In addition, the -q causes the99# command to exit with status code 1 instead of 128 in this case100# and to suppress the message to stderr.101args = ['symbolic-ref', '-q', 'HEAD']102output = cls.run_command(103args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location,104)105ref = output.strip()106107if ref.startswith('refs/heads/'):108return ref[len('refs/heads/'):]109110return None111112def export(self, location, url):113# type: (str, HiddenText) -> None114"""Export the Git repository at the url to the destination location"""115if not location.endswith('/'):116location = location + '/'117118with TempDirectory(kind="export") as temp_dir:119self.unpack(temp_dir.path, url=url)120self.run_command(121['checkout-index', '-a', '-f', '--prefix', location],122show_stdout=False, cwd=temp_dir.path123)124125@classmethod126def get_revision_sha(cls, dest, rev):127"""128Return (sha_or_none, is_branch), where sha_or_none is a commit hash129if the revision names a remote branch or tag, otherwise None.130131Args:132dest: the repository directory.133rev: the revision name.134"""135# Pass rev to pre-filter the list.136output = cls.run_command(['show-ref', rev], cwd=dest,137show_stdout=False, on_returncode='ignore')138refs = {}139for line in output.strip().splitlines():140try:141sha, ref = line.split()142except ValueError:143# Include the offending line to simplify troubleshooting if144# this error ever occurs.145raise ValueError('unexpected show-ref line: {!r}'.format(line))146147refs[ref] = sha148149branch_ref = 'refs/remotes/origin/{}'.format(rev)150tag_ref = 'refs/tags/{}'.format(rev)151152sha = refs.get(branch_ref)153if sha is not None:154return (sha, True)155156sha = refs.get(tag_ref)157158return (sha, False)159160@classmethod161def resolve_revision(cls, dest, url, rev_options):162# type: (str, HiddenText, RevOptions) -> RevOptions163"""164Resolve a revision to a new RevOptions object with the SHA1 of the165branch, tag, or ref if found.166167Args:168rev_options: a RevOptions object.169"""170rev = rev_options.arg_rev171# The arg_rev property's implementation for Git ensures that the172# rev return value is always non-None.173assert rev is not None174175sha, is_branch = cls.get_revision_sha(dest, rev)176177if sha is not None:178rev_options = rev_options.make_new(sha)179rev_options.branch_name = rev if is_branch else None180181return rev_options182183# Do not show a warning for the common case of something that has184# the form of a Git commit hash.185if not looks_like_hash(rev):186logger.warning(187"Did not find branch or tag '%s', assuming revision or ref.",188rev,189)190191if not rev.startswith('refs/'):192return rev_options193194# If it looks like a ref, we have to fetch it explicitly.195cls.run_command(196make_command('fetch', '-q', url, rev_options.to_args()),197cwd=dest,198)199# Change the revision to the SHA of the ref we fetched200sha = cls.get_revision(dest, rev='FETCH_HEAD')201rev_options = rev_options.make_new(sha)202203return rev_options204205@classmethod206def is_commit_id_equal(cls, dest, name):207"""208Return whether the current commit hash equals the given name.209210Args:211dest: the repository directory.212name: a string name.213"""214if not name:215# Then avoid an unnecessary subprocess call.216return False217218return cls.get_revision(dest) == name219220def fetch_new(self, dest, url, rev_options):221# type: (str, HiddenText, RevOptions) -> None222rev_display = rev_options.to_display()223logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest))224self.run_command(make_command('clone', '-q', url, dest))225226if rev_options.rev:227# Then a specific revision was requested.228rev_options = self.resolve_revision(dest, url, rev_options)229branch_name = getattr(rev_options, 'branch_name', None)230if branch_name is None:231# Only do a checkout if the current commit id doesn't match232# the requested revision.233if not self.is_commit_id_equal(dest, rev_options.rev):234cmd_args = make_command(235'checkout', '-q', rev_options.to_args(),236)237self.run_command(cmd_args, cwd=dest)238elif self.get_current_branch(dest) != branch_name:239# Then a specific branch was requested, and that branch240# is not yet checked out.241track_branch = 'origin/{}'.format(branch_name)242cmd_args = [243'checkout', '-b', branch_name, '--track', track_branch,244]245self.run_command(cmd_args, cwd=dest)246247#: repo may contain submodules248self.update_submodules(dest)249250def switch(self, dest, url, rev_options):251# type: (str, HiddenText, RevOptions) -> None252self.run_command(253make_command('config', 'remote.origin.url', url),254cwd=dest,255)256cmd_args = make_command('checkout', '-q', rev_options.to_args())257self.run_command(cmd_args, cwd=dest)258259self.update_submodules(dest)260261def update(self, dest, url, rev_options):262# type: (str, HiddenText, RevOptions) -> None263# First fetch changes from the default remote264if self.get_git_version() >= parse_version('1.9.0'):265# fetch tags in addition to everything else266self.run_command(['fetch', '-q', '--tags'], cwd=dest)267else:268self.run_command(['fetch', '-q'], cwd=dest)269# Then reset to wanted revision (maybe even origin/master)270rev_options = self.resolve_revision(dest, url, rev_options)271cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args())272self.run_command(cmd_args, cwd=dest)273#: update submodules274self.update_submodules(dest)275276@classmethod277def get_remote_url(cls, location):278"""279Return URL of the first remote encountered.280281Raises RemoteNotFoundError if the repository does not have a remote282url configured.283"""284# We need to pass 1 for extra_ok_returncodes since the command285# exits with return code 1 if there are no matching lines.286stdout = cls.run_command(287['config', '--get-regexp', r'remote\..*\.url'],288extra_ok_returncodes=(1, ), show_stdout=False, cwd=location,289)290remotes = stdout.splitlines()291try:292found_remote = remotes[0]293except IndexError:294raise RemoteNotFoundError295296for remote in remotes:297if remote.startswith('remote.origin.url '):298found_remote = remote299break300url = found_remote.split(' ')[1]301return url.strip()302303@classmethod304def get_revision(cls, location, rev=None):305if rev is None:306rev = 'HEAD'307current_rev = cls.run_command(308['rev-parse', rev], show_stdout=False, cwd=location,309)310return current_rev.strip()311312@classmethod313def get_subdirectory(cls, location):314"""315Return the path to setup.py, relative to the repo root.316Return None if setup.py is in the repo root.317"""318# find the repo root319git_dir = cls.run_command(320['rev-parse', '--git-dir'],321show_stdout=False, cwd=location).strip()322if not os.path.isabs(git_dir):323git_dir = os.path.join(location, git_dir)324repo_root = os.path.abspath(os.path.join(git_dir, '..'))325return find_path_to_setup_from_repo_root(location, repo_root)326327@classmethod328def get_url_rev_and_auth(cls, url):329# type: (str) -> Tuple[str, Optional[str], AuthInfo]330"""331Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'.332That's required because although they use SSH they sometimes don't333work with a ssh:// scheme (e.g. GitHub). But we need a scheme for334parsing. Hence we remove it again afterwards and return it as a stub.335"""336# Works around an apparent Git bug337# (see https://article.gmane.org/gmane.comp.version-control.git/146500)338scheme, netloc, path, query, fragment = urlsplit(url)339if scheme.endswith('file'):340initial_slashes = path[:-len(path.lstrip('/'))]341newpath = (342initial_slashes +343urllib_request.url2pathname(path)344.replace('\\', '/').lstrip('/')345)346url = urlunsplit((scheme, netloc, newpath, query, fragment))347after_plus = scheme.find('+') + 1348url = scheme[:after_plus] + urlunsplit(349(scheme[after_plus:], netloc, newpath, query, fragment),350)351352if '://' not in url:353assert 'file:' not in url354url = url.replace('git+', 'git+ssh://')355url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url)356url = url.replace('ssh://', '')357else:358url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url)359360return url, rev, user_pass361362@classmethod363def update_submodules(cls, location):364if not os.path.exists(os.path.join(location, '.gitmodules')):365return366cls.run_command(367['submodule', 'update', '--init', '--recursive', '-q'],368cwd=location,369)370371@classmethod372def get_repository_root(cls, location):373loc = super(Git, cls).get_repository_root(location)374if loc:375return loc376try:377r = cls.run_command(378['rev-parse', '--show-toplevel'],379cwd=location,380show_stdout=False,381on_returncode='raise',382log_failed_cmd=False,383)384except BadCommand:385logger.debug("could not determine if %s is under git control "386"because git is not available", location)387return None388except InstallationError:389return None390return os.path.normpath(r.rstrip('\r\n'))391392393vcs.register(Git)394395396