Skip to content

Commit

Permalink
Merge pull request #1650 from EliahKagan/envcase
Browse files Browse the repository at this point in the history
Fix Windows environment variable upcasing bug
  • Loading branch information
Byron authored Sep 7, 2023
2 parents 8017421 + eebdb25 commit 09e1b3d
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 21 deletions.
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."""
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"):
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"
if old_name == old_name.upper():
raise RuntimeError("test bug or strange locale: old_name invariant under upcasing")
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

0 comments on commit 09e1b3d

Please sign in to comment.