diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3aff2..b9c974e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Bugfix: Fetch the LFS objects of a `git checkout` of a tag or a commit as well. Previously, only a checkout of a branch triggered an LFS fetch of the mirror and the LFS objects of a tag or commit were downloaded by the git-lfs filters of the checkout itself. +- Bugfix: Skip the gitcache executable itself when searching for the real git command. Previously, only a symlink + named `git` was skipped, so that renaming or hard-linking the gitcache executable to `git` resulted in an + infinite recursion. As the check now compares the candidates with the gitcache executable, a symlink pointing to + the real git command is no longer skipped either. ## v1.0.34 diff --git a/src/git_cache/config.py b/src/git_cache/config.py index ebc9d4a..9596221 100644 --- a/src/git_cache/config.py +++ b/src/git_cache/config.py @@ -20,6 +20,7 @@ import os import platform import re +import sys from typing import Any, Callable, Dict, List, Optional import pytimeparse @@ -92,9 +93,11 @@ def find_git() -> str: cmd = "git.exe" if on_windows else "git" for candidate in path.split(os.path.pathsep): candidate = os.path.join(candidate, cmd) - if os.path.exists(candidate) and os.access(candidate, os.X_OK): - if not os.path.islink(candidate): - return candidate + if not os.path.exists(candidate) or not os.access(candidate, os.X_OK): + continue + if os.path.samefile(candidate, sys.executable): + continue + return candidate LOG.warning("Can't find git command! Please specify manually in the config file!") return "/usr/bin/git" diff --git a/test/unittests/test_git_cache_config.py b/test/unittests/test_git_cache_config.py index c096a54..3f30783 100644 --- a/test/unittests/test_git_cache_config.py +++ b/test/unittests/test_git_cache_config.py @@ -13,6 +13,10 @@ # ----------------------------------------------------------------------------- import importlib import os +import platform +import shutil +import sys +import tempfile from unittest import TestCase import mock @@ -157,6 +161,67 @@ def test_str_rep(self): self.assertEqual(str(config), expected_config_str) +class GitCacheFindGitTest(TestCase): + """Test the :func:`git_cache.config.find_git` function.""" + + ON_WINDOWS = platform.system().lower().startswith("win") + GIT_CMD = "git.exe" if ON_WINDOWS else "git" + GITCACHE_CMD = "gitcache.exe" if ON_WINDOWS else "gitcache" + + def setUp(self): + """Set up the test case with a gitcache executable and a real git command.""" + self.tmp_dir = tempfile.mkdtemp() + self.gitcache_cmd = self._create_executable(self.tmp_dir, self.GITCACHE_CMD) + + self.real_git_dir = os.path.join(self.tmp_dir, "real") + os.makedirs(self.real_git_dir) + self.real_git_cmd = self._create_executable(self.real_git_dir, self.GIT_CMD) + + self.gitcache_dir = os.path.join(self.tmp_dir, "gitcache_bin") + os.makedirs(self.gitcache_dir) + + def tearDown(self): + """Tear down the test case.""" + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + @staticmethod + def _create_executable(directory, name): + """Create an executable file and return its path.""" + path = os.path.join(directory, name) + with open(path, "w", encoding="utf-8") as file_handle: + file_handle.write("#!/bin/sh\nexit 0\n") + os.chmod(path, 0o755) + return path + + def _find_git(self, executable): + """Call find_git() as the given executable with the gitcache dir in front of the real git dir.""" + path = os.path.pathsep.join([self.gitcache_dir, self.real_git_dir]) + with mock.patch.object(sys, "executable", executable), mockenv(PATH=path): + return git_cache.config.find_git() + + def test_gitcache_executable(self): + """git_cache.config.find_git: Find the real git command.""" + self.assertEqual(self._find_git(self.gitcache_cmd), self.real_git_cmd) + + def test_skip_git_symlink(self): + """git_cache.config.find_git: Skip a symlink to the gitcache executable.""" + try: + os.symlink(self.gitcache_cmd, os.path.join(self.gitcache_dir, self.GIT_CMD)) + except OSError: + self.skipTest("This system does not allow the creation of symlinks.") + self.assertEqual(self._find_git(self.gitcache_cmd), self.real_git_cmd) + + def test_skip_git_hardlink(self): + """git_cache.config.find_git: Skip a hard link to the gitcache executable.""" + os.link(self.gitcache_cmd, os.path.join(self.gitcache_dir, self.GIT_CMD)) + self.assertEqual(self._find_git(self.gitcache_cmd), self.real_git_cmd) + + def test_skip_git_executable(self): + """git_cache.config.find_git: Skip the gitcache executable itself named git.""" + gitcache_as_git = self._create_executable(self.gitcache_dir, self.GIT_CMD) + self.assertEqual(self._find_git(gitcache_as_git), self.real_git_cmd) + + # ----------------------------------------------------------------------------- # EOF # -----------------------------------------------------------------------------