Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Windows environment variable upcasing bug #1650

Merged
merged 4 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 4 additions & 5 deletions git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import subprocess
import threading
from textwrap import dedent
import unittest.mock

from git.compat import (
defenc,
Expand All @@ -24,7 +23,7 @@
is_win,
)
from git.exc import CommandError
from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present
from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present, patch_env

from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError
from .util import (
Expand Down Expand Up @@ -965,10 +964,10 @@ def execute(
'"kill_after_timeout" feature is not supported on Windows.',
)
# Only search PATH, not CWD. This must be in the *caller* environment. The "1" can be any value.
patch_caller_env = unittest.mock.patch.dict(os.environ, {"NoDefaultCurrentDirectoryInExePath": "1"})
maybe_patch_caller_env = patch_env("NoDefaultCurrentDirectoryInExePath", "1")
else:
cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable
patch_caller_env = contextlib.nullcontext()
maybe_patch_caller_env = contextlib.nullcontext()
# end handle

stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
Expand All @@ -984,7 +983,7 @@ def execute(
istream_ok,
)
try:
with patch_caller_env:
with maybe_patch_caller_env:
proc = Popen(
command,
env=env,
Expand Down
17 changes: 16 additions & 1 deletion git/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T:

@contextlib.contextmanager
def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]:
"""Context manager to temporarily change directory. Not reentrant."""
Byron marked this conversation as resolved.
Show resolved Hide resolved
old_dir = os.getcwd()
os.chdir(new_dir)
try:
Expand All @@ -158,6 +159,20 @@ def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]:
os.chdir(old_dir)


@contextlib.contextmanager
def patch_env(name: str, value: str) -> Generator[None, None, None]:
"""Context manager to temporarily patch an environment variable."""
old_value = os.getenv(name)
os.environ[name] = value
try:
yield
finally:
if old_value is None:
del os.environ[name]
else:
os.environ[name] = old_value


def rmtree(path: PathLike) -> None:
"""Remove the given recursively.

Expand Down Expand Up @@ -935,7 +950,7 @@ def _obtain_lock_or_raise(self) -> None:
)

try:
with open(lock_file, mode='w'):
with open(lock_file, mode="w"):
Byron marked this conversation as resolved.
Show resolved Hide resolved
pass
except OSError as e:
raise IOError(str(e)) from e
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/env_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import subprocess
import sys

import git


_, working_dir, env_var_name = sys.argv

# Importing git should be enough, but this really makes sure Git.execute is called.
repo = git.Repo(working_dir) # Hold the reference.
git.Git(repo.working_dir).execute(["git", "version"])

print(subprocess.check_output(["set", env_var_name], shell=True, text=True))
35 changes: 20 additions & 15 deletions test/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,23 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import contextlib
import os
import shutil
import subprocess
import sys
from tempfile import TemporaryDirectory, TemporaryFile
from unittest import mock
from unittest import mock, skipUnless

from git import Git, refresh, GitCommandError, GitCommandNotFound, Repo, cmd
from test.lib import TestBase, fixture_path
from test.lib import with_rw_directory
from git.util import finalize_process
from git.util import cwd, finalize_process

import os.path as osp

from git.compat import is_win


@contextlib.contextmanager
def _chdir(new_dir):
"""Context manager to temporarily change directory. Not reentrant."""
old_dir = os.getcwd()
os.chdir(new_dir)
try:
yield
finally:
os.chdir(old_dir)


class TestGit(TestBase):
@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -102,9 +90,26 @@ def test_it_executes_git_not_from_cwd(self):
print("#!/bin/sh", file=file)
os.chmod(impostor_path, 0o755)

with _chdir(tmpdir):
with cwd(tmpdir):
self.assertRegex(self.git.execute(["git", "version"]), r"^git version\b")

@skipUnless(is_win, "The regression only affected Windows, and this test logic is OS-specific.")
def test_it_avoids_upcasing_unrelated_environment_variable_names(self):
old_name = "28f425ca_d5d8_4257_b013_8d63166c8158"
Byron marked this conversation as resolved.
Show resolved Hide resolved
if old_name == old_name.upper():
raise RuntimeError("test bug or strange locale: old_name invariant under upcasing")
Byron marked this conversation as resolved.
Show resolved Hide resolved
os.putenv(old_name, "1") # It has to be done this lower-level way to set it lower-case.

cmdline = [
sys.executable,
fixture_path("env_case.py"),
self.rorepo.working_dir,
old_name,
]
pair_text = subprocess.check_output(cmdline, shell=False, text=True)
new_name = pair_text.split("=")[0]
self.assertEqual(new_name, old_name)

def test_it_accepts_stdin(self):
filename = fixture_path("cat_file_blob")
with open(filename, "r") as fh:
Expand Down