diff --git a/changelogs/fragments/77493-ansible-galaxy-find-git-executable-before-using.yaml b/changelogs/fragments/77493-ansible-galaxy-find-git-executable-before-using.yaml new file mode 100644 index 00000000000..835bd6a9268 --- /dev/null +++ b/changelogs/fragments/77493-ansible-galaxy-find-git-executable-before-using.yaml @@ -0,0 +1,2 @@ +bugfixes: + - Fix traceback when installing a collection from a git repository and git is not installed (https://github.com/ansible/ansible/issues/77479). diff --git a/lib/ansible/galaxy/collection/concrete_artifact_manager.py b/lib/ansible/galaxy/collection/concrete_artifact_manager.py index 9aa450eb03f..5d2e61c949d 100644 --- a/lib/ansible/galaxy/collection/concrete_artifact_manager.py +++ b/lib/ansible/galaxy/collection/concrete_artifact_manager.py @@ -30,6 +30,7 @@ from ansible.galaxy import get_collections_galaxy_meta_info from ansible.galaxy.dependency_resolution.dataclasses import _GALAXY_YAML from ansible.galaxy.user_agent import user_agent from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.process import get_bin_path from ansible.module_utils.common.yaml import yaml_load from ansible.module_utils.six import raise_from from ansible.module_utils.urls import open_url @@ -393,11 +394,19 @@ def _extract_collection_from_git(repo_url, coll_ver, b_path): prefix=to_bytes(name, errors='surrogate_or_strict'), ) # type: bytes + try: + git_executable = get_bin_path('git') + except ValueError as err: + raise AnsibleError( + "Could not find git executable to extract the collection from the Git repository `{repo_url!s}`.". + format(repo_url=to_native(git_url)) + ) from err + # Perform a shallow clone if simply cloning HEAD if version == 'HEAD': - git_clone_cmd = 'git', 'clone', '--depth=1', git_url, to_text(b_checkout_path) + git_clone_cmd = git_executable, 'clone', '--depth=1', git_url, to_text(b_checkout_path) else: - git_clone_cmd = 'git', 'clone', git_url, to_text(b_checkout_path) + git_clone_cmd = git_executable, 'clone', git_url, to_text(b_checkout_path) # FIXME: '--branch', version try: @@ -411,7 +420,7 @@ def _extract_collection_from_git(repo_url, coll_ver, b_path): proc_err, ) - git_switch_cmd = 'git', 'checkout', to_text(version) + git_switch_cmd = git_executable, 'checkout', to_text(version) try: subprocess.check_call(git_switch_cmd, cwd=b_checkout_path) except subprocess.CalledProcessError as proc_err: diff --git a/test/units/galaxy/test_collection_install.py b/test/units/galaxy/test_collection_install.py index 41b2d0818e9..e34472f2e68 100644 --- a/test/units/galaxy/test_collection_install.py +++ b/test/units/galaxy/test_collection_install.py @@ -18,6 +18,7 @@ import yaml from io import BytesIO, StringIO from mock import MagicMock, patch +from unittest import mock import ansible.module_utils.six.moves.urllib.error as urllib_error @@ -27,6 +28,7 @@ from ansible.errors import AnsibleError from ansible.galaxy import collection, api, dependency_resolution from ansible.galaxy.dependency_resolution.dataclasses import Candidate, Requirement from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.process import get_bin_path from ansible.utils import context_objects as co from ansible.utils.display import Display @@ -172,6 +174,22 @@ def galaxy_server(): return galaxy_api +def test_concrete_artifact_manager_scm_no_executable(monkeypatch): + url = 'https://github.com/org/repo' + version = 'commitish' + mock_subprocess_check_call = MagicMock() + monkeypatch.setattr(collection.concrete_artifact_manager.subprocess, 'check_call', mock_subprocess_check_call) + mock_mkdtemp = MagicMock(return_value='') + monkeypatch.setattr(collection.concrete_artifact_manager, 'mkdtemp', mock_mkdtemp) + + error = re.escape( + "Could not find git executable to extract the collection from the Git repository `https://github.com/org/repo`" + ) + with mock.patch.dict(os.environ, {"PATH": ""}): + with pytest.raises(AnsibleError, match=error): + collection.concrete_artifact_manager._extract_collection_from_git(url, version, b'path') + + @pytest.mark.parametrize( 'url,version,trailing_slash', [ @@ -194,10 +212,12 @@ def test_concrete_artifact_manager_scm_cmd(url, version, trailing_slash, monkeyp repo = 'https://github.com/org/repo' if trailing_slash: repo += '/' - clone_cmd = ('git', 'clone', repo, '') + + git_executable = get_bin_path('git') + clone_cmd = (git_executable, 'clone', repo, '') assert mock_subprocess_check_call.call_args_list[0].args[0] == clone_cmd - assert mock_subprocess_check_call.call_args_list[1].args[0] == ('git', 'checkout', 'commitish') + assert mock_subprocess_check_call.call_args_list[1].args[0] == (git_executable, 'checkout', 'commitish') @pytest.mark.parametrize( @@ -223,10 +243,11 @@ def test_concrete_artifact_manager_scm_cmd_shallow(url, version, trailing_slash, repo = 'https://github.com/org/repo' if trailing_slash: repo += '/' - shallow_clone_cmd = ('git', 'clone', '--depth=1', repo, '') + git_executable = get_bin_path('git') + shallow_clone_cmd = (git_executable, 'clone', '--depth=1', repo, '') assert mock_subprocess_check_call.call_args_list[0].args[0] == shallow_clone_cmd - assert mock_subprocess_check_call.call_args_list[1].args[0] == ('git', 'checkout', 'HEAD') + assert mock_subprocess_check_call.call_args_list[1].args[0] == (git_executable, 'checkout', 'HEAD') def test_build_requirement_from_path(collection_artifact):