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

Address issue #4675: fix Git.check_version() #4676

Merged
merged 8 commits into from
Oct 5, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions docs/reference/pip_install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ Passing branch names, a commit hash or a tag name is possible like so::
[-e] git://git.example.com/MyProject.git@v1.0#egg=MyProject
[-e] git://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject

When passing a commit hash, specifying a full hash is preferable to a partial
hash because a full hash allows pip to operate more efficiently (e.g. by
making fewer network calls).

Mercurial
~~~~~~~~~

Expand Down
3 changes: 3 additions & 0 deletions news/4675.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix an issue where ``pip install -e`` on a Git url would fail to update if
a branch or tag name is specified that happens to match the prefix of the
current ``HEAD`` commit hash.
13 changes: 6 additions & 7 deletions src/pip/_internal/vcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,13 @@ def update(self, dest, rev_options):
"""
raise NotImplementedError

def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""
Return True if the version is identical to what exists and
doesn't need to be updated.
Return whether the id of the current commit equals the given name.

Args:
rev_options: a RevOptions object.
dest: the repository directory.
name: a string name.
"""
raise NotImplementedError

Expand Down Expand Up @@ -313,7 +313,7 @@ def check_destination(self, dest, url, rev_options):
display_path(dest),
url,
)
if not self.check_version(dest, rev_options):
if not self.is_commit_id_equal(dest, rev_options.rev):
logger.info(
'Updating %s %s%s',
display_path(dest),
Expand Down Expand Up @@ -405,8 +405,7 @@ def get_url(self, location):

def get_revision(self, location):
"""
Return the current revision of the files at location
Used in get_info
Return the current commit id of the files at the given location.
"""
raise NotImplementedError

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 @@ -104,7 +104,7 @@ def get_src_requirement(self, dist, location):
current_rev = self.get_revision(location)
return '%s@%s#egg=%s' % (repo, current_rev, egg_project_name)

def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""Always assume the versions don't match"""
return False

Expand Down
23 changes: 13 additions & 10 deletions src/pip/_internal/vcs/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,19 @@ def check_rev_options(self, dest, rev_options):

return rev_options

def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""
Compare the current sha to the ref. ref may be a branch or tag name,
but current rev will always point to a sha. This means that a branch
or tag will never compare as True. So this ultimately only matches
against exact shas.
Return whether the current commit hash equals the given name.

Args:
rev_options: a RevOptions object.
dest: the repository directory.
name: a string name.
"""
return self.get_revision(dest).startswith(rev_options.arg_rev)
if not name:
# Then avoid an unnecessary subprocess call.
return False

return self.get_revision(dest) == name
Copy link
Member

@xavfernandez xavfernandez Oct 4, 2017

Choose a reason for hiding this comment

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

I don't like the fact that 2b466712a067a171e8d5a0fe6210e004cfe103a8 won't be equal to 2b466712a.

Would self.get_revision(dest) == self.run_command(['rev-parse',name], show_stdout=False, cwd=location work ?

Copy link
Member

@xavfernandez xavfernandez Oct 5, 2017

Choose a reason for hiding this comment

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

After thinking some more I guess the point of this PR is that 2b466712a could also be the name of a branch so my remark is moot.
If one wants to avoid an useless update, one should specify the whole commit hash. An additionnal note in https://pip.pypa.io/en/stable/reference/pip_install/#git might be welcome :)

Copy link
Member

Choose a reason for hiding this comment

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

But the thing you're checking is user-supplied, and requiring people to provide a full hash is pretty user-unfriendly. We should match git's behaviour (which is to allow prefixes as long as they don't match a branch or tag, as I understand it).

Disclaimer: I've never used this feature, and don't anticipate a need to. So if people who actually use it are happy with requiring a full hash, then their opinions should override mine.

Copy link
Member Author

@cjerdonek cjerdonek Oct 5, 2017

Choose a reason for hiding this comment

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

[Replying to @xavfernandez] Right. For example, if you look at the original docstring, you'll see that the function isn't ever supposed to return True for branches or tags. A more realistic example might be an integer tag like "10". An accidental SHA prefix match could cause the tag not to be installed.

Specifying a SHA prefix should still work to install a SHA. It just won't incorrectly "shadow" a branch or tag, which is what this PR addresses.

Copy link
Member Author

Choose a reason for hiding this comment

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

@pfmoore Providing a partial SHA still works. The user isn't required to provide a full hash. This PR just addresses an optimization code path. The optimization was implemented not quite correctly. It was implemented so that if a user specified a tag that accidentally matches the beginning of a SHA, the tag wouldn't get installed (because the optimization bypasses fetching new tags).

Copy link
Member

@xavfernandez xavfernandez Oct 5, 2017

Choose a reason for hiding this comment

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

@pfmoore specifying a short hash will still work but pip will update the source in this case instead of assuming equality.
Cf https://gist.github.com/xavfernandez/47ac526e0c6bd0ada872deba2c3c2c05 for the difference between pip 9.0.1 and this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for taking the time to do those examples, @xavfernandez.

Copy link
Member

Choose a reason for hiding this comment

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

No problem, if @pfmoore agrees I'd say the only thing missing before merging would be a small note in https://github.com/pypa/pip/blob/master/docs/reference/pip_install.rst#git if it's not too much to ask :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, would be happy to do that. I'll see if I can do that now.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for clarifying, yes I'm happy for this to go in with the note as requested.


def switch(self, dest, url, rev_options):
self.run_command(['config', 'remote.origin.url', url], cwd=dest)
Expand Down Expand Up @@ -164,10 +166,11 @@ def obtain(self, dest):

if rev:
rev_options = self.check_rev_options(dest, rev_options)
# Only do a checkout if rev_options differs from HEAD
if not self.check_version(dest, 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):
cmd_args = ['fetch', '-q', url] + rev_options.to_args()
self.run_command(cmd_args, cwd=dest,)
self.run_command(cmd_args, cwd=dest)
self.run_command(
['checkout', '-q', 'FETCH_HEAD'],
cwd=dest,
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 @@ -97,7 +97,7 @@ def get_src_requirement(self, dist, location):
current_rev_hash = self.get_revision_hash(location)
return '%s@%s#egg=%s' % (repo, current_rev_hash, egg_project_name)

def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""Always assume the versions don't match"""
return False

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 @@ -217,7 +217,7 @@ def get_src_requirement(self, dist, location):
rev = self.get_revision(location)
return 'svn+%s@%s#egg=%s' % (repo, rev, egg_project_name)

def check_version(self, dest, rev_options):
def is_commit_id_equal(self, dest, name):
"""Always assume the versions don't match"""
return False

Expand Down
20 changes: 10 additions & 10 deletions tests/functional/test_install_vcs_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,24 @@ def test_get_short_refs_should_ignore_no_branch(script):
assert result['branch0.1'] == commit, result


def call_check_version(vcs, path, rev):
rev_options = vcs.make_rev_options(rev)
return vcs.check_version(path, rev_options)


@pytest.mark.network
def test_check_version(script):
def test_is_commit_id_equal(script):
"""
Test Git.is_commit_id_equal().
"""
version_pkg_path = _create_test_package(script)
script.run('git', 'branch', 'branch0.1', cwd=version_pkg_path)
commit = script.run(
'git', 'rev-parse', 'HEAD',
cwd=version_pkg_path
).stdout.strip()
git = Git()
assert call_check_version(git, version_pkg_path, commit)
assert call_check_version(git, version_pkg_path, commit[:7])
assert not call_check_version(git, version_pkg_path, 'branch0.1')
assert not call_check_version(git, version_pkg_path, 'abc123')
assert git.is_commit_id_equal(version_pkg_path, commit)
assert not git.is_commit_id_equal(version_pkg_path, commit[:7])
assert not git.is_commit_id_equal(version_pkg_path, 'branch0.1')
assert not git.is_commit_id_equal(version_pkg_path, 'abc123')
# Also check passing a None value.
assert not git.is_commit_id_equal(version_pkg_path, None)


@patch('pip._internal.vcs.git.Git.get_short_refs')
Expand Down
14 changes: 9 additions & 5 deletions tests/unit/test_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,19 @@ def test_git_get_src_requirements(git, dist):
])


@pytest.mark.parametrize('ref,result', (
@pytest.mark.parametrize('rev_name,result', (
('5547fa909e83df8bd743d3978d6667497983a4b7', True),
('5547fa909', True),
('5547fa909', False),
('5678', False),
('abc123', False),
('foo', False),
(None, False),
))
def test_git_check_version(git, ref, result):
rev_options = git.make_rev_options(ref)
assert git.check_version('foo', rev_options) is result
def test_git_is_commit_id_equal(git, rev_name, result):
"""
Test Git.is_commit_id_equal().
"""
assert git.is_commit_id_equal('/path', rev_name) is result


def test_translate_egg_surname():
Expand Down