diff --git a/CHANGELOG.md b/CHANGELOG.md index 6175b0d..7af19dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ 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. +- Bugfix: Also skip the gitcache script when it is run by a Python interpreter, as in a pip installation. There, + `sys.executable` is the interpreter, so a `git` symlink to the installed `gitcache` script was returned as the + real git command whenever no `RealGit` was configured, e.g. with `HOME` pointing to a fresh directory, and each + call spawned itself until the system ran out of memory. - Bugfix: Keep the user of `ssh://` and scp-style URLs when normalizing URLs and stripping credentials. The user of an SSH URL is part of the address, not a secret. Dropping it left mirrors created from SSH URLs unable to authenticate on `gitcache -u` and whenever the same repository was reached through an HTTPS URL diff --git a/src/git_cache/config.py b/src/git_cache/config.py index 9596221..7b30d52 100644 --- a/src/git_cache/config.py +++ b/src/git_cache/config.py @@ -80,6 +80,19 @@ def has_git_lfs_cmd() -> bool: return has_git_lfs_cmd.has_git_lfs # type: ignore +def _is_gitcache_executable(candidate: str) -> bool: + """Check whether the given path is the running gitcache executable. + + A frozen gitcache is ``sys.executable``. A pip-installed gitcache is a script + started by the Python interpreter, so ``sys.executable`` is the interpreter and + the script is ``sys.argv[0]``. + """ + own_paths = [sys.executable] + if sys.argv and sys.argv[0]: + own_paths.append(sys.argv[0]) + return any(os.path.exists(own) and os.path.samefile(candidate, own) for own in own_paths) + + def find_git() -> str: """Locate the real git command. @@ -95,7 +108,7 @@ def find_git() -> str: candidate = os.path.join(candidate, cmd) if not os.path.exists(candidate) or not os.access(candidate, os.X_OK): continue - if os.path.samefile(candidate, sys.executable): + if _is_gitcache_executable(candidate): continue return candidate diff --git a/test/unittests/test_git_cache_config.py b/test/unittests/test_git_cache_config.py index 3f30783..f3f89da 100644 --- a/test/unittests/test_git_cache_config.py +++ b/test/unittests/test_git_cache_config.py @@ -193,10 +193,11 @@ def _create_executable(directory, name): os.chmod(path, 0o755) return path - def _find_git(self, executable): + def _find_git(self, executable, argv0=None): """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): + argv = [argv0 if argv0 is not None else executable] + with mock.patch.object(sys, "executable", executable), mock.patch.object(sys, "argv", argv), mockenv(PATH=path): return git_cache.config.find_git() def test_gitcache_executable(self): @@ -221,6 +222,21 @@ def test_skip_git_executable(self): gitcache_as_git = self._create_executable(self.gitcache_dir, self.GIT_CMD) self.assertEqual(self._find_git(gitcache_as_git), self.real_git_cmd) + def test_skip_git_symlink_to_script(self): + """git_cache.config.find_git: Skip a symlink to a gitcache script run by a separate interpreter.""" + interpreter = self._create_executable(self.tmp_dir, "python") + git_symlink = os.path.join(self.gitcache_dir, self.GIT_CMD) + try: + os.symlink(self.gitcache_cmd, git_symlink) + except OSError: + self.skipTest("This system does not allow the creation of symlinks.") + self.assertEqual(self._find_git(interpreter, argv0=git_symlink), self.real_git_cmd) + + def test_skip_without_argv0(self): + """git_cache.config.find_git: Ignore an empty sys.argv[0].""" + interpreter = self._create_executable(self.tmp_dir, "python") + self.assertEqual(self._find_git(interpreter, argv0=""), self.real_git_cmd) + # ----------------------------------------------------------------------------- # EOF