Skip to content

Commit 09e1b3d

Browse files
authored
Merge pull request #1650 from EliahKagan/envcase
Fix Windows environment variable upcasing bug
2 parents 8017421 + eebdb25 commit 09e1b3d

File tree

4 files changed

+53
-21
lines changed

4 files changed

+53
-21
lines changed

git/cmd.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import subprocess
1515
import threading
1616
from textwrap import dedent
17-
import unittest.mock
1817

1918
from git.compat import (
2019
defenc,
@@ -24,7 +23,7 @@
2423
is_win,
2524
)
2625
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
2827

2928
from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError
3029
from .util import (
@@ -965,10 +964,10 @@ def execute(
965964
'"kill_after_timeout" feature is not supported on Windows.',
966965
)
967966
# 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")
969968
else:
970969
cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable
971-
patch_caller_env = contextlib.nullcontext()
970+
maybe_patch_caller_env = contextlib.nullcontext()
972971
# end handle
973972

974973
stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
@@ -984,7 +983,7 @@ def execute(
984983
istream_ok,
985984
)
986985
try:
987-
with patch_caller_env:
986+
with maybe_patch_caller_env:
988987
proc = Popen(
989988
command,
990989
env=env,

git/util.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T:
150150

151151
@contextlib.contextmanager
152152
def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]:
153+
"""Context manager to temporarily change directory. Not reentrant."""
153154
old_dir = os.getcwd()
154155
os.chdir(new_dir)
155156
try:
@@ -158,6 +159,20 @@ def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]:
158159
os.chdir(old_dir)
159160

160161

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+
161176
def rmtree(path: PathLike) -> None:
162177
"""Remove the given recursively.
163178
@@ -935,7 +950,7 @@ def _obtain_lock_or_raise(self) -> None:
935950
)
936951

937952
try:
938-
with open(lock_file, mode='w'):
953+
with open(lock_file, mode="w"):
939954
pass
940955
except OSError as e:
941956
raise IOError(str(e)) from e

test/fixtures/env_case.py

+13
Original file line numberDiff line numberDiff line change
@@ -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))

test/test_git.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,23 @@
44
#
55
# This module is part of GitPython and is released under
66
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
7-
import contextlib
87
import os
98
import shutil
109
import subprocess
1110
import sys
1211
from tempfile import TemporaryDirectory, TemporaryFile
13-
from unittest import mock
12+
from unittest import mock, skipUnless
1413

1514
from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
1615
from test.lib import TestBase, fixture_path
1716
from test.lib import with_rw_directory
18-
from git.util import finalize_process
17+
from git.util import cwd, finalize_process
1918

2019
import os.path as osp
2120

2221
from git.compat import is_win
2322

2423

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-
3624
class TestGit(TestBase):
3725
@classmethod
3826
def setUpClass(cls):
@@ -102,9 +90,26 @@ def test_it_executes_git_not_from_cwd(self):
10290
print("#!/bin/sh", file=file)
10391
os.chmod(impostor_path, 0o755)
10492

105-
with _chdir(tmpdir):
93+
with cwd(tmpdir):
10694
self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b")
10795

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+
108113
def test_it_accepts_stdin(self):
109114
filename = fixture_path("cat_file_blob")
110115
with open(filename, "r") as fh:

0 commit comments

Comments
 (0)