Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #5624: installing a Git ref for installs other than the first #5623

Merged
merged 3 commits into from
Aug 17, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/5624.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix installing a Git ref for installs after the first.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be more clear, it was not obvious to me what this change was about until I actually looked at the code. Perhaps "Allow a git ref to be installed over an existing non-ref install"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll try rephrasing. The meaning was meant to be a bit more general actually -- allowing a Git ref to be installed over an existing install of any kind. Currently (without this PR), if it's the very first install (a "fresh" install), installing a Git ref works. But if it's the second or later install (i.e. an "update" to an install), then it doesn't work, no matter what the previous installs were. So the PR makes the latter case work, too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so maybe "Allow a git ref to be installed over an existing installation" then?

That said I think most casual users are not going to understand this change regardless so perhaps it's fine as is. 🙂

4 changes: 2 additions & 2 deletions src/pip/_internal/vcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def switch(self, dest, url, rev_options):
"""
raise NotImplementedError

def update(self, dest, rev_options):
def update(self, dest, url, rev_options):
"""
Update an already-existing repo to the given ``rev_options``.

Expand Down Expand Up @@ -345,7 +345,7 @@ def obtain(self, dest):
self.repo_name,
rev_display,
)
self.update(dest, rev_options)
self.update(dest, url, rev_options)
else:
logger.info('Skipping because already up-to-date.')
return
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/vcs/bazaar.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def fetch_new(self, dest, url, rev_options):
def switch(self, dest, url, rev_options):
self.run_command(['switch', url], cwd=dest)

def update(self, dest, rev_options):
def update(self, dest, url, rev_options):
cmd_args = ['pull', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)

Expand Down
46 changes: 26 additions & 20 deletions src/pip/_internal/vcs/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,10 @@ def get_revision_sha(self, dest, rev):

return refs.get(branch_ref) or refs.get(tag_ref)

def check_rev_options(self, dest, rev_options):
"""Check the revision options before checkout.

Returns a new RevOptions object for the SHA1 of the branch or tag
if found.
def resolve_revision(self, dest, url, rev_options):
"""
Resolve a revision to a new RevOptions object with the SHA1 of the
branch, tag, or ref if found.

Args:
rev_options: a RevOptions object.
Expand All @@ -139,6 +138,19 @@ def check_rev_options(self, dest, rev_options):
"Did not find branch or tag '%s', assuming revision or ref.",
rev,
)

if not rev.startswith('refs/'):
return rev_options

# If it looks like a ref, we have to fetch it explicitly.
self.run_command(
['fetch', '-q', url] + rev_options.to_args(),
cwd=dest,
)
# Change the revision to the SHA of the ref we fetched
sha = self.get_revision(dest, rev='FETCH_HEAD')
rev_options = rev_options.make_new(sha)

return rev_options

def is_commit_id_equal(self, dest, name):
Expand All @@ -164,20 +176,12 @@ def fetch_new(self, dest, url, rev_options):

if rev_options.rev:
# Then a specific revision was requested.
rev_options = self.check_rev_options(dest, rev_options)
rev_options = self.resolve_revision(dest, url, rev_options)
# Only do a checkout if the current commit id doesn't match
# the requested revision.
if not self.is_commit_id_equal(dest, rev_options.rev):
rev = rev_options.rev
# Only fetch the revision if it's a ref
if rev.startswith('refs/'):
self.run_command(
['fetch', '-q', url] + rev_options.to_args(),
cwd=dest,
)
# Change the revision to the SHA of the ref we fetched
rev = 'FETCH_HEAD'
self.run_command(['checkout', '-q', rev], cwd=dest)
cmd_args = ['checkout', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)

#: repo may contain submodules
self.update_submodules(dest)
Expand All @@ -189,15 +193,15 @@ def switch(self, dest, url, rev_options):

self.update_submodules(dest)

def update(self, dest, rev_options):
def update(self, dest, url, rev_options):
# First fetch changes from the default remote
if self.get_git_version() >= parse_version('1.9.0'):
# fetch tags in addition to everything else
self.run_command(['fetch', '-q', '--tags'], cwd=dest)
else:
self.run_command(['fetch', '-q'], cwd=dest)
# Then reset to wanted revision (maybe even origin/master)
rev_options = self.check_rev_options(dest, rev_options)
rev_options = self.resolve_revision(dest, url, rev_options)
cmd_args = ['reset', '--hard', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
#: update submodules
Expand All @@ -218,9 +222,11 @@ def get_url(self, location):
url = found_remote.split(' ')[1]
return url.strip()

def get_revision(self, location):
def get_revision(self, location, rev=None):
if rev is None:
rev = 'HEAD'
current_rev = self.run_command(
['rev-parse', 'HEAD'], show_stdout=False, cwd=location,
['rev-parse', rev], show_stdout=False, cwd=location,
)
return current_rev.strip()

Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/vcs/mercurial.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def switch(self, dest, url, rev_options):
cmd_args = ['update', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)

def update(self, dest, rev_options):
def update(self, dest, url, rev_options):
self.run_command(['pull', '-q'], cwd=dest)
cmd_args = ['update', '-q'] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest)
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/vcs/subversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def switch(self, dest, url, rev_options):
cmd_args = ['switch'] + rev_options.to_args() + [url, dest]
self.run_command(cmd_args)

def update(self, dest, rev_options):
def update(self, dest, url, rev_options):
cmd_args = ['update'] + rev_options.to_args() + [dest]
self.run_command(cmd_args)

Expand Down
59 changes: 44 additions & 15 deletions tests/functional/test_install_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,23 +180,52 @@ def test_git_with_tag_name_as_revision(script):
assert '0.1' in version.stdout


@pytest.mark.network
def test_git_with_ref_as_revision(script):
def _add_ref(script, path, ref):
"""
Git backend should be able to install from a ref
Add a new ref to a repository at the given path.
"""
version_pkg_path = _create_test_package(script)
script.run(
'git', 'update-ref', 'refs/foo/bar', 'HEAD',
expect_stderr=True,
cwd=version_pkg_path,
)
_change_test_package_version(script, version_pkg_path)
script.pip(
'install', '-e', '%s@refs/foo/bar#egg=version_pkg' %
('git+file://' + version_pkg_path.abspath.replace('\\', '/')),
expect_stderr=True
)
script.run('git', 'update-ref', ref, 'HEAD', expect_stderr=True, cwd=path)


def make_version_pkg_url(path, revision=None):
path = path.abspath.replace('\\', '/')
url_rev = '' if revision is None else '@' + revision
url = 'git+file://{}{}#egg=version_pkg'.format(path, url_rev)

return url


def test_git_install_ref(script):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like these tests lost the @pytest.mark.network decorator. I'm not 100% sure what the mark means in the context of this test suite, but all the other tests in this file have it, perhaps these two should as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator is essentially for marking all tests that reach out to the network.

Copy link
Member Author

@cjerdonek cjerdonek Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I removed it because _create_test_package() creates the source package on the file system, and the package is installed with the 'git+file:// scheme (see make_version_pkg_url() a few lines above).

I thought the network decorator was only meant for cases where the test was reaching out to an external network, like test_install_noneditable_git, which installs from GitHub (with url git+https://github.com/pypa/pip-test-package.git). It looked to me like some other tests in the file might also have the decorator unnecessarily.

Technically, I guess the Git docs do say that file:// "fires up the processes that it normally uses to transfer data over a network, which is generally much less efficient," but it seems like there is a big difference between using the same "processes" for network activity and actually reaching out to an external network location like, say, GitHub. What do you both think? I'm guessing the decorator is used to help partition tests for CI purposes (e.g. which tests are slower or depend on external networks, etc).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The partitioning is so that you can run tests while not connected to the internet.

If this test runs with no internet connectivity, then it doesn't need this decorator and vice versa.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll leave the decorator off then because as near as I can tell it doesn't require internet connectivity.

"""
The Git backend should be able to install a ref with the first install.
"""
package_path = _create_test_package(script)
_add_ref(script, package_path, 'refs/foo/bar')
_change_test_package_version(script, package_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to run this here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so because otherwise version.stdout for the ref will be the same as version.stdout for master (namely both 0.1), in which case we won't know if the test is succeeding because it's actually installing the ref or just because it happens to match master. By running _change_test_package_version(), it makes it so that version.stdout for the ref is 0.1 and version.stdout for master is some different version, so the test has to install the ref to succeed.


package_url = make_version_pkg_url(package_path, revision='refs/foo/bar')
script.pip('install', '-e', package_url, expect_stderr=True)
version = script.run('version_pkg')
assert '0.1' in version.stdout


def test_git_install_then_install_ref(script):
"""
The Git backend should be able to install a ref after a package has
already been installed.
"""
package_path = _create_test_package(script)
_add_ref(script, package_path, 'refs/foo/bar')
_change_test_package_version(script, package_path)

package_url = make_version_pkg_url(package_path)
script.pip('install', '-e', package_url, expect_stderr=True)
version = script.run('version_pkg')
assert 'some different version' in version.stdout

# Now install the ref.
package_url = make_version_pkg_url(package_path, revision='refs/foo/bar')
script.pip('install', '-e', package_url, expect_stderr=True)
version = script.run('version_pkg')
assert '0.1' in version.stdout

Expand Down
17 changes: 10 additions & 7 deletions tests/unit/test_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,37 +108,40 @@ def test_git_get_src_requirements(git, dist):


@patch('pip._internal.vcs.git.Git.get_revision_sha')
def test_git_check_rev_options_ref_exists(get_sha_mock):
def test_git_resolve_revision_rev_exists(get_sha_mock):
get_sha_mock.return_value = '123456'
git = Git()
rev_options = git.make_rev_options('develop')

new_options = git.check_rev_options('.', rev_options)
url = 'git+https://git.example.com'
new_options = git.resolve_revision('.', url, rev_options)
assert new_options.rev == '123456'


@patch('pip._internal.vcs.git.Git.get_revision_sha')
def test_git_check_rev_options_ref_not_found(get_sha_mock):
def test_git_resolve_revision_rev_not_found(get_sha_mock):
get_sha_mock.return_value = None
git = Git()
rev_options = git.make_rev_options('develop')

new_options = git.check_rev_options('.', rev_options)
url = 'git+https://git.example.com'
new_options = git.resolve_revision('.', url, rev_options)
assert new_options.rev == 'develop'


@patch('pip._internal.vcs.git.Git.get_revision_sha')
def test_git_check_rev_options_not_found_warning(get_sha_mock, caplog):
def test_git_resolve_revision_not_found_warning(get_sha_mock, caplog):
get_sha_mock.return_value = None
git = Git()

url = 'git+https://git.example.com'
sha = 40 * 'a'
rev_options = git.make_rev_options(sha)
new_options = git.check_rev_options('.', rev_options)
new_options = git.resolve_revision('.', url, rev_options)
assert new_options.rev == sha

rev_options = git.make_rev_options(sha[:6])
new_options = git.check_rev_options('.', rev_options)
new_options = git.resolve_revision('.', url, rev_options)
assert new_options.rev == 'aaaaaa'

# Check that a warning got logged only for the abbreviated hash.
Expand Down