Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d1e140f

Browse files
committed
Fixups: Code fixups to get all the tests to pass
Fixups: Code fixups after squashing Fixups: Fix test-requirements
2 parents 598191b + 2a21f09 commit d1e140f

File tree

9 files changed

+187
-364
lines changed

9 files changed

+187
-364
lines changed

errortext.txt

Lines changed: 0 additions & 329 deletions
This file was deleted.

git/cmd.py

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66

77
import contextlib
8+
import re
9+
810
import io
911
import logging
1012
import os
@@ -130,6 +132,59 @@ def pump_stream(cmdline, name, stream, is_decode, handler):
130132
return finalizer(process)
131133

132134

135+
def _safer_popen_windows(command, shell, env=None, **kwargs):
136+
"""Call :class:`subprocess.Popen` on Windows but don't include a CWD in the search.
137+
This avoids an untrusted search path condition where a file like ``git.exe`` in a
138+
malicious repository would be run when GitPython operates on the repository. The
139+
process using GitPython may have an untrusted repository's working tree as its
140+
current working directory. Some operations may temporarily change to that directory
141+
before running a subprocess. In addition, while by default GitPython does not run
142+
external commands with a shell, it can be made to do so, in which case the CWD of
143+
the subprocess, which GitPython usually sets to a repository working tree, can
144+
itself be searched automatically by the shell. This wrapper covers all those cases.
145+
:note: This currently works by setting the ``NoDefaultCurrentDirectoryInExePath``
146+
environment variable during subprocess creation. It also takes care of passing
147+
Windows-specific process creation flags, but that is unrelated to path search.
148+
:note: The current implementation contains a race condition on :attr:`os.environ`.
149+
GitPython isn't thread-safe, but a program using it on one thread should ideally
150+
be able to mutate :attr:`os.environ` on another, without unpredictable results.
151+
See comments in https://github.com/gitpython-developers/GitPython/pull/1650.
152+
"""
153+
# CREATE_NEW_PROCESS_GROUP is needed for some ways of killing it afterwards. See:
154+
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
155+
# https://docs.python.org/3/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
156+
creationflags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
157+
158+
# When using a shell, the shell is the direct subprocess, so the variable must be
159+
# set in its environment, to affect its search behavior. (The "1" can be any value.)
160+
if shell:
161+
safer_env = {} if env is None else dict(env)
162+
safer_env["NoDefaultCurrentDirectoryInExePath"] = "1"
163+
else:
164+
safer_env = env
165+
166+
# When not using a shell, the current process does the search in a CreateProcessW
167+
# API call, so the variable must be set in our environment. With a shell, this is
168+
# unnecessary, in versions where https://github.com/python/cpython/issues/101283 is
169+
# patched. If not, in the rare case the ComSpec environment variable is unset, the
170+
# shell is searched for unsafely. Setting NoDefaultCurrentDirectoryInExePath in all
171+
# cases, as here, is simpler and protects against that. (The "1" can be any value.)
172+
with patch_env("NoDefaultCurrentDirectoryInExePath", "1"):
173+
return Popen(
174+
command,
175+
shell=shell,
176+
env=safer_env,
177+
creationflags=creationflags,
178+
**kwargs
179+
)
180+
181+
182+
if os.name == "nt":
183+
safer_popen = _safer_popen_windows
184+
else:
185+
safer_popen = Popen
186+
187+
133188
def dashify(string):
134189
return string.replace('_', '-')
135190

@@ -150,11 +205,6 @@ def dict_to_slots_and__excluded_are_none(self, d, excluded=()):
150205
# value of Windows process creation flag taken from MSDN
151206
CREATE_NO_WINDOW = 0x08000000
152207

153-
## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards,
154-
# see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal
155-
PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
156-
if is_win else 0)
157-
158208

159209
class Git(LazyMixin):
160210

@@ -771,21 +821,18 @@ def execute(self, command,
771821
log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)",
772822
command, cwd, universal_newlines, shell, istream_ok)
773823
try:
774-
with patch_caller_env:
775-
proc = Popen(
776-
command,
777-
env=env,
778-
cwd=cwd,
779-
bufsize=-1,
780-
stdin=istream,
781-
stderr=PIPE,
782-
stdout=stdout_sink,
783-
shell=shell is not None and shell or self.USE_SHELL,
784-
close_fds=is_posix, # unsupported on windows
785-
universal_newlines=universal_newlines,
786-
creationflags=PROC_CREATIONFLAGS,
787-
**subprocess_kwargs
788-
)
824+
proc = safer_popen(
825+
command,
826+
env=env,
827+
cwd=cwd,
828+
bufsize=-1,
829+
stdin=istream,
830+
stderr=PIPE,
831+
stdout=stdout_sink,
832+
shell=shell is not None and shell or self.USE_SHELL,
833+
universal_newlines=universal_newlines,
834+
**subprocess_kwargs
835+
)
789836
except cmd_not_found_exception as err:
790837
raise GitCommandNotFound(command, err)
791838

git/index/fun.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
import subprocess
1515

16-
from git.cmd import PROC_CREATIONFLAGS, handle_process_output
16+
from git.cmd import handle_process_output, safer_popen
1717
from git.compat import (
1818
PY3,
1919
defenc,
@@ -73,15 +73,12 @@ def run_commit_hook(name, index, *args):
7373
env["GIT_INDEX_FILE"] = safe_decode(index.path) if PY3 else safe_encode(index.path)
7474
env["GIT_EDITOR"] = ":"
7575
try:
76-
cmd = subprocess.Popen(
77-
[hp] + list(args),
78-
env=env,
79-
stdout=subprocess.PIPE,
80-
stderr=subprocess.PIPE,
81-
cwd=index.repo.working_dir,
82-
close_fds=is_posix,
83-
creationflags=PROC_CREATIONFLAGS,
84-
)
76+
cmd = safer_popen([hp] + list(args),
77+
env=env,
78+
stdout=subprocess.PIPE,
79+
stderr=subprocess.PIPE,
80+
cwd=index.repo.working_dir,
81+
close_fds=is_posix)
8582
except Exception as ex:
8683
raise HookExecutionError(hp, ex)
8784
else:

git/remote.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ def push(
911911
be null."""
912912
kwargs = add_progress(kwargs, self.repo.git, progress)
913913

914-
refspec = Git._unpack_args(refspec or [])
914+
refspec = Git._Git__unpack_args(refspec or [])
915915
if not allow_unsafe_protocols:
916916
for ref in refspec:
917917
Git.check_unsafe_protocols(ref)

git/test/lib/helper.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import textwrap
1717
import time
1818
import unittest
19+
import virtualenv
1920

2021
from git.compat import string_types, is_win
2122
from git.util import rmtree, cwd
@@ -36,7 +37,8 @@
3637
__all__ = (
3738
'fixture_path', 'fixture', 'StringProcessAdapter',
3839
'with_rw_directory', 'with_rw_repo', 'with_rw_and_rw_remote_repo',
39-
'TestBase', 'TestCase',
40+
'TestBase', 'VirtualEnvironment',
41+
'TestCase',
4042
'SkipTest', 'skipIf',
4143
'GIT_REPO', 'GIT_DAEMON_PORT'
4244
)
@@ -83,13 +85,13 @@ def with_rw_directory(func):
8385
test succeeds, but leave it otherwise to aid additional debugging"""
8486

8587
@wraps(func)
86-
def wrapper(self):
88+
def wrapper(self, *args, **kwargs):
8789
path = tempfile.mktemp(prefix=func.__name__)
8890
os.mkdir(path)
8991
keep = False
9092
try:
9193
try:
92-
return func(self, path)
94+
return func(self, path, *args, **kwargs)
9395
except Exception:
9496
log.info("Test %s.%s failed, output is at %r\n",
9597
type(self).__name__, func.__name__, path)
@@ -379,3 +381,49 @@ def _make_file(self, rela_path, data, repo=None):
379381
with open(abs_path, "w") as fp:
380382
fp.write(data)
381383
return abs_path
384+
385+
386+
class VirtualEnvironment:
387+
"""A newly created Python virtual environment for use in a test."""
388+
389+
__slots__ = ("_env_dir",)
390+
391+
def __init__(self, env_dir, with_pip):
392+
# On Python2 virtualenv the pip option and symlinks options aren't available
393+
if os.name == "nt":
394+
self._env_dir = osp.realpath(env_dir)
395+
# venv.create(self.env_dir, symlinks=False, with_pip=with_pip)
396+
virtualenv.cli_run([self.env_dir])
397+
else:
398+
self._env_dir = env_dir
399+
# venv.create(self.env_dir, symlinks=True, with_pip=with_pip)
400+
virtualenv.cli_run([self.env_dir])
401+
402+
@property
403+
def env_dir(self):
404+
"""The top-level directory of the environment."""
405+
return self._env_dir
406+
407+
@property
408+
def python(self):
409+
"""Path to the Python executable in the environment."""
410+
return self._executable("python")
411+
412+
@property
413+
def pip(self):
414+
"""Path to the pip executable in the environment, or RuntimeError if absent."""
415+
return self._executable("pip")
416+
417+
@property
418+
def sources(self):
419+
"""Path to a src directory in the environment, which may not exist yet."""
420+
return os.path.join(self.env_dir, "src")
421+
422+
def _executable(self, basename):
423+
if os.name == "nt":
424+
path = osp.join(self.env_dir, "Scripts", basename + ".exe")
425+
else:
426+
path = osp.join(self.env_dir, "bin", basename)
427+
if osp.isfile(path) or osp.islink(path):
428+
return path
429+
raise RuntimeError("no regular file or symlink " + str(path))

git/test/test_git.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
except ImportError:
4545
import mock
4646

47+
try:
48+
from pathlib import Path
49+
except ImportError:
50+
from pathlib2 import Path
51+
4752
from git.compat import is_win
4853

4954
@contextlib.contextmanager
@@ -57,6 +62,22 @@ def _chdir(new_dir):
5762
os.chdir(old_dir)
5863

5964

65+
@contextlib.contextmanager
66+
def _patch_out_env(name):
67+
try:
68+
old_value = os.environ[name]
69+
except KeyError:
70+
old_value = None
71+
else:
72+
del os.environ[name]
73+
try:
74+
yield
75+
finally:
76+
if old_value is not None:
77+
os.environ[name] = old_value
78+
79+
80+
6081
class TestGit(TestBase):
6182

6283
@classmethod
@@ -321,7 +342,7 @@ def counter_stderr(line):
321342
stdout=subprocess.PIPE,
322343
stderr=subprocess.PIPE,
323344
shell=False,
324-
creationflags=cmd.PROC_CREATIONFLAGS,
345+
# creationflags=cmd.PROC_CREATIONFLAGS,
325346
)
326347

327348
handle_process_output(proc, counter_stdout, counter_stderr, finalize_process)

git/test/test_repo.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ def test_clone_from_pathlib_withConfig(self, rw_dir):
262262

263263
@with_rw_repo("HEAD")
264264
def test_clone_unsafe_options(self, rw_repo):
265+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
266+
raise SkipTest("pathlib was introduced in 3.4")
267+
265268
tmp_dir = pathlib.Path(tempfile.mkdtemp())
266269
tmp_file = tmp_dir / "pwn"
267270
unsafe_options = [
@@ -321,6 +324,9 @@ def test_clone_unsafe_options(self, rw_repo):
321324

322325
@with_rw_repo("HEAD")
323326
def test_clone_safe_options(self, rw_repo):
327+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
328+
raise SkipTest("pathlib was introduced in 3.4")
329+
324330
tmp_dir = pathlib.Path(tempfile.mkdtemp())
325331
options = [
326332
"--depth=1",
@@ -335,6 +341,9 @@ def test_clone_safe_options(self, rw_repo):
335341

336342
@with_rw_repo("HEAD")
337343
def test_clone_from_unsafe_options(self, rw_repo):
344+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
345+
raise SkipTest("pathlib was introduced in 3.4")
346+
338347
tmp_dir = pathlib.Path(tempfile.mkdtemp())
339348
tmp_file = tmp_dir / "pwn"
340349
unsafe_options = [
@@ -400,6 +409,9 @@ def test_clone_from_unsafe_options(self, rw_repo):
400409

401410
@with_rw_repo("HEAD")
402411
def test_clone_from_safe_options(self, rw_repo):
412+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
413+
raise SkipTest("pathlib was introduced in 3.4")
414+
403415
tmp_dir = pathlib.Path(tempfile.mkdtemp())
404416
options = [
405417
"--depth=1",
@@ -413,6 +425,9 @@ def test_clone_from_safe_options(self, rw_repo):
413425
assert destination.exists()
414426

415427
def test_clone_from_unsafe_procol(self):
428+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
429+
raise SkipTest("pathlib was introduced in 3.4")
430+
416431
tmp_dir = pathlib.Path(tempfile.mkdtemp())
417432
tmp_file = tmp_dir / "pwn"
418433
urls = [
@@ -425,6 +440,9 @@ def test_clone_from_unsafe_procol(self):
425440
assert not tmp_file.exists()
426441

427442
def test_clone_from_unsafe_procol_allowed(self):
443+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
444+
raise SkipTest("pathlib was introduced in 3.4")
445+
428446
tmp_dir = pathlib.Path(tempfile.mkdtemp())
429447
tmp_file = tmp_dir / "pwn"
430448
urls = [
@@ -1286,6 +1304,9 @@ def test_git_work_tree_env(self, rw_dir):
12861304

12871305
@with_rw_repo("HEAD")
12881306
def test_clone_command_injection(self, rw_repo):
1307+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
1308+
raise SkipTest("pathlib was introduced in 3.4")
1309+
12891310
tmp_dir = pathlib.Path(tempfile.mkdtemp())
12901311
unexpected_file = tmp_dir / "pwn"
12911312
assert not unexpected_file.exists()
@@ -1297,6 +1318,9 @@ def test_clone_command_injection(self, rw_repo):
12971318

12981319
@with_rw_repo("HEAD")
12991320
def test_clone_from_command_injection(self, rw_repo):
1321+
if pathlib is None: # pythons bellow 3.4 don't have pathlib
1322+
raise SkipTest("pathlib was introduced in 3.4")
1323+
13001324
tmp_dir = pathlib.Path(tempfile.mkdtemp())
13011325
temp_repo = Repo.init(tmp_dir / "repo")
13021326
unexpected_file = tmp_dir / "pwn"

git/util.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,17 @@ def _get_exe_extensions():
192192

193193

194194
def py_where(program, path=None):
195+
"""Perform a path search to assist :func:`is_cygwin_git`.
196+
This is not robust for general use. It is an implementation detail of
197+
:func:`is_cygwin_git`. When a search following all shell rules is needed,
198+
:func:`shutil.which` can be used instead.
199+
:note: Neither this function nor :func:`shutil.which` will predict the effect of an
200+
executable search on a native Windows system due to a :class:`subprocess.Popen`
201+
call without ``shell=True``, because shell and non-shell executable search on
202+
Windows differ considerably.
203+
"""
204+
205+
195206
# From: http://stackoverflow.com/a/377028/548792
196207
winprog_exts = _get_exe_extensions()
197208

test-requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ coverage
44
flake8
55
nose
66
tox
7+
mock; python_version=='2.7'
8+
pathlib2; python_version=='2.7'
79
backports.tempfile
810
six
11+
virtualenv
12+

0 commit comments

Comments
 (0)