@@ -14,7 +14,6 @@ | |||
| 14 | 14 | import subprocess | |
| 15 | 15 | import threading | |
| 16 | 16 | from textwrap import dedent | |
| 17 | - import unittest.mock | ||
| 18 | 17 | ||
| 19 | 18 | from git.compat import ( | |
| 20 | 19 | defenc, | |
@@ -24,7 +23,7 @@ | |||
| 24 | 23 | is_win, | |
| 25 | 24 | ) | |
| 26 | 25 | from git.exc import CommandError | |
| 27 | - from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present | ||
| 26 | + from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present, patch_env | ||
| 28 | 27 | ||
| 29 | 28 | from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError | |
| 30 | 29 | from .util import ( | |
@@ -965,10 +964,10 @@ def execute( | |||
| 965 | 964 | '"kill_after_timeout" feature is not supported on Windows.', | |
| 966 | 965 | ) | |
| 967 | 966 | # Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value. | |
| 968 | - patch_caller_env = unittest.mock.patch.dict(os.environ, {"NoDefaultCurrentDirectoryInExePath": "1"}) | ||
| 967 | + maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1") | ||
| 969 | 968 | else: | |
| 970 | 969 | cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable | |
| 971 | - patch_caller_env = contextlib.nullcontext() | ||
| 970 | + maybe_patch_caller_env = contextlib.nullcontext() | ||
| 972 | 971 | # end handle | |
| 973 | 972 | ||
| 974 | 973 | stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") | |
@@ -984,7 +983,7 @@ def execute( | |||
| 984 | 983 | istream_ok, | |
| 985 | 984 | ) | |
| 986 | 985 | try: | |
| 987 | - with patch_caller_env: | ||
| 986 | + with maybe_patch_caller_env: | ||
| 988 | 987 | proc = Popen( | |
| 989 | 988 | command, | |
| 990 | 989 | env=env, | |
@@ -150,6 +150,7 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T: | |||
| 150 | 150 | ||
| 151 | 151 | @contextlib.contextmanager | |
| 152 | 152 | def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: | |
| 153 | + """Context manager to temporarily change directory. Not reentrant.""" | ||
| 153 | 154 | old_dir = os.getcwd() | |
| 154 | 155 | os.chdir(new_dir) | |
| 155 | 156 | try: | |
@@ -158,6 +159,20 @@ def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: | |||
| 158 | 159 | os.chdir(old_dir) | |
| 159 | 160 | ||
| 160 | 161 | ||
| 162 | + @contextlib.contextmanager | ||
| 163 | + def patch_env(name: str, value: str) -> Generator[None, None, None]: | ||
| 164 | + """Context manager to temporarily patch an environment variable.""" | ||
| 165 | + old_value = os.getenv(name) | ||
| 166 | + os.environ[name] = value | ||
| 167 | + try: | ||
| 168 | + yield | ||
| 169 | + finally: | ||
| 170 | + if old_value is None: | ||
| 171 | + del os.environ[name] | ||
| 172 | + else: | ||
| 173 | + os.environ[name] = old_value | ||
| 174 | + | ||
| 175 | + | ||
| 161 | 176 | def rmtree(path: PathLike) -> None: | |
| 162 | 177 | """Remove the given recursively. | |
| 163 | 178 | ||
@@ -935,7 +950,7 @@ def _obtain_lock_or_raise(self) -> None: | |||
| 935 | 950 | ) | |
| 936 | 951 | ||
| 937 | 952 | try: | |
| 938 | - with open(lock_file, mode='w'): | ||
| 953 | + with open(lock_file, mode="w"): | ||
| 939 | 954 | pass | |
| 940 | 955 | except OSError as e: | |
| 941 | 956 | raise IOError(str(e)) from e | |
@@ -0,0 +1,13 @@ | |||
| 1 | + import subprocess | ||
| 2 | + import sys | ||
| 3 | + | ||
| 4 | + import git | ||
| 5 | + | ||
| 6 | + | ||
| 7 | + _, working_dir, env_var_name = sys.argv | ||
| 8 | + | ||
| 9 | + # Importing git should be enough, but this really makes sure Git.execute is called. | ||
| 10 | + repo = git.Repo(working_dir) # Hold the reference. | ||
| 11 | + git.Git(repo.working_dir).execute(["git", "version"]) | ||
| 12 | + | ||
| 13 | + print(subprocess.check_output(["set", env_var_name], shell=True, text=True)) | ||
@@ -4,35 +4,23 @@ | |||
| 4 | 4 | # | |
| 5 | 5 | # This module is part of GitPython and is released under | |
| 6 | 6 | # the BSD License: http://www.opensource.org/licenses/bsd-license.php | |
| 7 | - import contextlib | ||
| 8 | 7 | import os | |
| 9 | 8 | import shutil | |
| 10 | 9 | import subprocess | |
| 11 | 10 | import sys | |
| 12 | 11 | from tempfile import TemporaryDirectory, TemporaryFile | |
| 13 | - from unittest import mock | ||
| 12 | + from unittest import mock, skipUnless | ||
| 14 | 13 | ||
| 15 | 14 | from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd | |
| 16 | 15 | from test.lib import TestBase, fixture_path | |
| 17 | 16 | from test.lib import with_rw_directory | |
| 18 | - from git.util import finalize_process | ||
| 17 | + from git.util import cwd, finalize_process | ||
| 19 | 18 | ||
| 20 | 19 | import os.path as osp | |
| 21 | 20 | ||
| 22 | 21 | from git.compat import is_win | |
| 23 | 22 | ||
| 24 | 23 | ||
| 25 | - @contextlib.contextmanager | ||
| 26 | - def _chdir(new_dir): | ||
| 27 | - """Context manager to temporarily change directory. Not reentrant.""" | ||
| 28 | - old_dir = os.getcwd() | ||
| 29 | - os.chdir(new_dir) | ||
| 30 | - try: | ||
| 31 | - yield | ||
| 32 | - finally: | ||
| 33 | - os.chdir(old_dir) | ||
| 34 | - | ||
| 35 | - | ||
| 36 | 24 | class TestGit(TestBase): | |
| 37 | 25 | @classmethod | |
| 38 | 26 | def setUpClass(cls): | |
@@ -102,9 +90,26 @@ def test_it_executes_git_not_from_cwd(self): | |||
| 102 | 90 | print("#!/bin/sh", file=file) | |
| 103 | 91 | os.chmod(impostor_path, 0o755) | |
| 104 | 92 | ||
| 105 | - with _chdir(tmpdir): | ||
| 93 | + with cwd(tmpdir): | ||
| 106 | 94 | self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b") | |
| 107 | 95 | ||
| 96 | + @skipUnless(is_win, "The regression only affected Windows, and this test logic is OS-specific.") | ||
| 97 | + def test_it_avoids_upcasing_unrelated_environment_variable_names(self): | ||
| 98 | + old_name = "28f425ca_d5d8_4257_b013_8d63166c8158" | ||
| 99 | + if old_name == old_name.upper(): | ||
| 100 | + raise RuntimeError("test bug or strange locale: old_name invariant under upcasing") | ||
| 101 | + os.putenv(old_name, "1") # It has to be done this lower-level way to set it lower-case. | ||
| 102 | + | ||
| 103 | + cmdline = [ | ||
| 104 | + sys.executable, | ||
| 105 | + fixture_path("env_case.py"), | ||
| 106 | + self.rorepo.working_dir, | ||
| 107 | + old_name, | ||
| 108 | + ] | ||
| 109 | + pair_text = subprocess.check_output(cmdline, shell=False, text=True) | ||
| 110 | + new_name = pair_text.split("=")[0] | ||
| 111 | + self.assertEqual(new_name, old_name) | ||
| 112 | + | ||
| 108 | 113 | def test_it_accepts_stdin(self): | |
| 109 | 114 | filename = fixture_path("cat_file_blob") | |
| 110 | 115 | with open(filename, "r") as fh: | |
0 commit comments