-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9822 from bwoodsend/fix-pip-freeze-git
- Loading branch information
Showing
7 changed files
with
163 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Fix :ref:`pip freeze` to output packages :ref:`installed from git <vcs support>` | ||
in the correct ``git+protocol://git.example.com/MyProject#egg=MyProject`` format | ||
rather than the old and no longer supported ``git+git@`` format. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import logging | ||
import os.path | ||
import pathlib | ||
import re | ||
import urllib.parse | ||
import urllib.request | ||
|
@@ -14,6 +15,7 @@ | |
from pip._internal.vcs.versioncontrol import ( | ||
AuthInfo, | ||
RemoteNotFoundError, | ||
RemoteNotValidError, | ||
RevOptions, | ||
VersionControl, | ||
find_path_to_setup_from_repo_root, | ||
|
@@ -29,6 +31,18 @@ | |
|
||
HASH_REGEX = re.compile('^[a-fA-F0-9]{40}$') | ||
|
||
# SCP (Secure copy protocol) shorthand. e.g. '[email protected]:foo/bar.git' | ||
SCP_REGEX = re.compile(r"""^ | ||
# Optional user, e.g. 'git@' | ||
(\w+@)? | ||
# Server, e.g. 'github.com'. | ||
([^/:]+): | ||
# The server-side path. e.g. 'user/project.git'. Must start with an | ||
# alphanumeric character so as not to be confusable with a Windows paths | ||
# like 'C:/foo/bar' or 'C:\foo\bar'. | ||
(\w[^:]*) | ||
$""", re.VERBOSE) | ||
|
||
|
||
def looks_like_hash(sha): | ||
# type: (str) -> bool | ||
|
@@ -328,7 +342,39 @@ def get_remote_url(cls, location): | |
found_remote = remote | ||
break | ||
url = found_remote.split(' ')[1] | ||
return url.strip() | ||
return cls._git_remote_to_pip_url(url.strip()) | ||
|
||
@staticmethod | ||
def _git_remote_to_pip_url(url): | ||
# type: (str) -> str | ||
""" | ||
Convert a remote url from what git uses to what pip accepts. | ||
There are 3 legal forms **url** may take: | ||
1. A fully qualified url: ssh://[email protected]/foo/bar.git | ||
2. A local project.git folder: /path/to/bare/repository.git | ||
3. SCP shorthand for form 1: [email protected]:foo/bar.git | ||
Form 1 is output as-is. Form 2 must be converted to URI and form 3 must | ||
be converted to form 1. | ||
See the corresponding test test_git_remote_url_to_pip() for examples of | ||
sample inputs/outputs. | ||
""" | ||
if re.match(r"\w+://", url): | ||
# This is already valid. Pass it though as-is. | ||
return url | ||
if os.path.exists(url): | ||
# A local bare remote (git clone --mirror). | ||
# Needs a file:// prefix. | ||
return pathlib.PurePath(url).as_uri() | ||
scp_match = SCP_REGEX.match(url) | ||
if scp_match: | ||
# Add an ssh:// prefix and replace the ':' with a '/'. | ||
return scp_match.expand(r"ssh://\1\2/\3") | ||
# Otherwise, bail out. | ||
raise RemoteNotValidError(url) | ||
|
||
@classmethod | ||
def has_commit(cls, location, rev): | ||
|
@@ -446,5 +492,12 @@ def get_repository_root(cls, location): | |
return None | ||
return os.path.normpath(r.rstrip('\r\n')) | ||
|
||
@staticmethod | ||
def should_add_vcs_url_prefix(repo_url): | ||
# type: (str) -> bool | ||
"""In either https or ssh form, requirements must be prefixed with git+. | ||
""" | ||
return True | ||
|
||
|
||
vcs.register(Git) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import os | ||
import pathlib | ||
from unittest import TestCase | ||
from unittest.mock import patch | ||
|
||
|
@@ -9,7 +10,7 @@ | |
from pip._internal.utils.misc import hide_url, hide_value | ||
from pip._internal.vcs import make_vcs_requirement_url | ||
from pip._internal.vcs.bazaar import Bazaar | ||
from pip._internal.vcs.git import Git, looks_like_hash | ||
from pip._internal.vcs.git import Git, RemoteNotValidError, looks_like_hash | ||
from pip._internal.vcs.mercurial import Mercurial | ||
from pip._internal.vcs.subversion import Subversion | ||
from pip._internal.vcs.versioncontrol import RevOptions, VersionControl | ||
|
@@ -108,37 +109,98 @@ def test_looks_like_hash(sha, expected): | |
|
||
|
||
@pytest.mark.parametrize('vcs_cls, remote_url, expected', [ | ||
# Git is one of the subclasses using the base class implementation. | ||
(Git, 'git://example.com/MyProject', False), | ||
# Mercurial is one of the subclasses using the base class implementation. | ||
# `hg://` isn't a real prefix but it tests the default behaviour. | ||
(Mercurial, 'hg://[email protected]/MyProject', False), | ||
(Mercurial, 'http://example.com/MyProject', True), | ||
# The Git subclasses should return true in all cases. | ||
(Git, 'git://example.com/MyProject', True), | ||
(Git, 'http://example.com/MyProject', True), | ||
# Subversion is the only subclass overriding the base class implementation. | ||
# Subversion also overrides the base class implementation. | ||
(Subversion, 'svn://example.com/MyProject', True), | ||
]) | ||
def test_should_add_vcs_url_prefix(vcs_cls, remote_url, expected): | ||
actual = vcs_cls.should_add_vcs_url_prefix(remote_url) | ||
assert actual == expected | ||
|
||
|
||
@pytest.mark.parametrize("url, target", [ | ||
# A fully qualified remote url. No changes needed. | ||
("ssh://bob@server/foo/bar.git", "ssh://bob@server/foo/bar.git"), | ||
("git://bob@server/foo/bar.git", "git://bob@server/foo/bar.git"), | ||
# User is optional and does not need a default. | ||
("ssh://server/foo/bar.git", "ssh://server/foo/bar.git"), | ||
# The common scp shorthand for ssh remotes. Pip won't recognise these as | ||
# git remotes until they have a 'ssh://' prefix and the ':' in the middle | ||
# is gone. | ||
("[email protected]:foo/bar.git", "ssh://[email protected]/foo/bar.git"), | ||
("example.com:foo.git", "ssh://example.com/foo.git"), | ||
# Http(s) remote names are already complete and should remain unchanged. | ||
("https://example.com/foo", "https://example.com/foo"), | ||
("http://example.com/foo/bar.git", "http://example.com/foo/bar.git"), | ||
("https://[email protected]/foo", "https://[email protected]/foo"), | ||
]) | ||
def test_git_remote_url_to_pip(url, target): | ||
assert Git._git_remote_to_pip_url(url) == target | ||
|
||
|
||
@pytest.mark.parametrize("url, platform", [ | ||
# Windows paths with the ':' drive prefix look dangerously close to SCP. | ||
("c:/piffle/wiffle/waffle/poffle.git", "nt"), | ||
(r"c:\faffle\waffle\woffle\piffle.git", "nt"), | ||
# Unix paths less so but test them anyway. | ||
("/muffle/fuffle/pufffle/fluffle.git", "posix"), | ||
]) | ||
def test_paths_are_not_mistaken_for_scp_shorthand(url, platform): | ||
# File paths should not be mistaken for SCP shorthand. If they do then | ||
# 'c:/piffle/wiffle' would end up as 'ssh://c/piffle/wiffle'. | ||
from pip._internal.vcs.git import SCP_REGEX | ||
assert not SCP_REGEX.match(url) | ||
|
||
if platform == os.name: | ||
with pytest.raises(RemoteNotValidError): | ||
Git._git_remote_to_pip_url(url) | ||
|
||
|
||
def test_git_remote_local_path(tmpdir): | ||
path = pathlib.Path(tmpdir, "project.git") | ||
path.mkdir() | ||
# Path must exist to be recognised as a local git remote. | ||
assert Git._git_remote_to_pip_url(str(path)) == path.as_uri() | ||
|
||
|
||
@patch('pip._internal.vcs.git.Git.get_remote_url') | ||
@patch('pip._internal.vcs.git.Git.get_revision') | ||
@patch('pip._internal.vcs.git.Git.get_subdirectory') | ||
@pytest.mark.parametrize( | ||
"git_url, target_url_prefix", | ||
[ | ||
( | ||
"https://github.com/pypa/pip-test-package", | ||
"git+https://github.com/pypa/pip-test-package", | ||
), | ||
( | ||
"[email protected]:pypa/pip-test-package", | ||
"git+ssh://[email protected]/pypa/pip-test-package", | ||
), | ||
], | ||
ids=["https", "ssh"], | ||
) | ||
@pytest.mark.network | ||
def test_git_get_src_requirements( | ||
mock_get_subdirectory, mock_get_revision, mock_get_remote_url | ||
mock_get_subdirectory, mock_get_revision, mock_get_remote_url, | ||
git_url, target_url_prefix, | ||
): | ||
git_url = 'https://github.com/pypa/pip-test-package' | ||
sha = '5547fa909e83df8bd743d3978d6667497983a4b7' | ||
|
||
mock_get_remote_url.return_value = git_url | ||
mock_get_remote_url.return_value = Git._git_remote_to_pip_url(git_url) | ||
mock_get_revision.return_value = sha | ||
mock_get_subdirectory.return_value = None | ||
|
||
ret = Git.get_src_requirement('.', 'pip-test-package') | ||
|
||
assert ret == ( | ||
'git+https://github.com/pypa/pip-test-package' | ||
'@5547fa909e83df8bd743d3978d6667497983a4b7#egg=pip_test_package' | ||
) | ||
target = f"{target_url_prefix}@{sha}#egg=pip_test_package" | ||
assert ret == target | ||
|
||
|
||
@patch('pip._internal.vcs.git.Git.get_revision_sha') | ||
|