5
5
6
6
from __future__ import annotations
7
7
8
- __all__ = ["Git" ]
8
+ __all__ = ["GitMeta" , " Git" ]
9
9
10
10
import contextlib
11
11
import io
19
19
import sys
20
20
from textwrap import dedent
21
21
import threading
22
+ import warnings
22
23
23
24
from git .compat import defenc , force_bytes , safe_decode
24
25
from git .exc import (
@@ -307,8 +308,79 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc
307
308
308
309
## -- End Utilities -- @}
309
310
311
+ _USE_SHELL_DEFAULT_MESSAGE = (
312
+ "Git.USE_SHELL is deprecated, because only its default value of False is safe. "
313
+ "It will be removed in a future release."
314
+ )
315
+
316
+ _USE_SHELL_DANGER_MESSAGE = (
317
+ "Setting Git.USE_SHELL to True is unsafe and insecure, as the effect of special "
318
+ "shell syntax cannot usually be accounted for. This can result in a command "
319
+ "injection vulnerability and arbitrary code execution. Git.USE_SHELL is deprecated "
320
+ "and will be removed in a future release."
321
+ )
322
+
323
+
324
+ def _warn_use_shell (extra_danger : bool ) -> None :
325
+ warnings .warn (
326
+ _USE_SHELL_DANGER_MESSAGE if extra_danger else _USE_SHELL_DEFAULT_MESSAGE ,
327
+ DeprecationWarning ,
328
+ stacklevel = 3 ,
329
+ )
330
+
331
+
332
+ class _GitMeta (type ):
333
+ """Metaclass for :class:`Git`.
334
+
335
+ This helps issue :class:`DeprecationWarning` if :attr:`Git.USE_SHELL` is used.
336
+ """
337
+
338
+ def __getattribute (cls , name : str ) -> Any :
339
+ if name == "USE_SHELL" :
340
+ _warn_use_shell (False )
341
+ return super ().__getattribute__ (name )
342
+
343
+ def __setattr (cls , name : str , value : Any ) -> Any :
344
+ if name == "USE_SHELL" :
345
+ _warn_use_shell (value )
346
+ super ().__setattr__ (name , value )
347
+
348
+ if not TYPE_CHECKING :
349
+ # To preserve static checking for undefined/misspelled attributes while letting
350
+ # the methods' bodies be type-checked, these are defined as non-special methods,
351
+ # then bound to special names out of view of static type checkers. (The original
352
+ # names invoke name mangling (leading "__") to avoid confusion in other scopes.)
353
+ __getattribute__ = __getattribute
354
+ __setattr__ = __setattr
355
+
356
+
357
+ GitMeta = _GitMeta
358
+ """Alias of :class:`Git`'s metaclass, whether it is :class:`type` or a custom metaclass.
359
+
360
+ Whether the :class:`Git` class has the default :class:`type` as its metaclass or uses a
361
+ custom metaclass is not documented and may change at any time. This statically checkable
362
+ metaclass alias is equivalent at runtime to ``type(Git)``. This should almost never be
363
+ used. Code that benefits from it is likely to be remain brittle even if it is used.
310
364
311
- class Git :
365
+ In view of the :class:`Git` class's intended use and :class:`Git` objects' dynamic
366
+ callable attributes representing git subcommands, it rarely makes sense to inherit from
367
+ :class:`Git` at all. Using :class:`Git` in multiple inheritance can be especially tricky
368
+ to do correctly. Attempting uses of :class:`Git` where its metaclass is relevant, such
369
+ as when a sibling class has an unrelated metaclass and a shared lower bound metaclass
370
+ might have to be introduced to solve a metaclass conflict, is not recommended.
371
+
372
+ :note:
373
+ The correct static type of the :class:`Git` class itself, and any subclasses, is
374
+ ``Type[Git]``. (This can be written as ``type[Git]`` in Python 3.9 later.)
375
+
376
+ :class:`GitMeta` should never be used in any annotation where ``Type[Git]`` is
377
+ intended or otherwise possible to use. This alias is truly only for very rare and
378
+ inherently precarious situations where it is necessary to deal with the metaclass
379
+ explicitly.
380
+ """
381
+
382
+
383
+ class Git (metaclass = _GitMeta ):
312
384
"""The Git class manages communication with the Git binary.
313
385
314
386
It provides a convenient interface to calling the Git binary, such as in::
@@ -358,24 +430,53 @@ def __setstate__(self, d: Dict[str, Any]) -> None:
358
430
GIT_PYTHON_TRACE = os .environ .get ("GIT_PYTHON_TRACE" , False )
359
431
"""Enables debugging of GitPython's git commands."""
360
432
361
- USE_SHELL = False
433
+ USE_SHELL : bool = False
362
434
"""Deprecated. If set to ``True``, a shell will be used when executing git commands.
363
435
436
+ Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
437
+ functions should be updated to use the default value of ``False`` instead. ``True``
438
+ is unsafe unless the effect of syntax treated specially by the shell is fully
439
+ considered and accounted for, which is not possible under most circumstances. As
440
+ detailed below, it is also no longer needed, even where it had been in the past.
441
+
442
+ It is in many if not most cases a command injection vulnerability for an application
443
+ to set :attr:`USE_SHELL` to ``True``. Any attacker who can cause a specially crafted
444
+ fragment of text to make its way into any part of any argument to any git command
445
+ (including paths, branch names, etc.) can cause the shell to read and write
446
+ arbitrary files and execute arbitrary commands. Innocent input may also accidentally
447
+ contain special shell syntax, leading to inadvertent malfunctions.
448
+
449
+ In addition, how a value of ``True`` interacts with some aspects of GitPython's
450
+ operation is not precisely specified and may change without warning, even before
451
+ GitPython 4.0.0 when :attr:`USE_SHELL` may be removed. This includes:
452
+
453
+ * Whether or how GitPython automatically customizes the shell environment.
454
+
455
+ * Whether, outside of Windows (where :class:`subprocess.Popen` supports lists of
456
+ separate arguments even when ``shell=True``), this can be used with any GitPython
457
+ functionality other than direct calls to the :meth:`execute` method.
458
+
459
+ * Whether any GitPython feature that runs git commands ever attempts to partially
460
+ sanitize data a shell may treat specially. Currently this is not done.
461
+
364
462
Prior to GitPython 2.0.8, this had a narrow purpose in suppressing console windows
365
463
in graphical Windows applications. In 2.0.8 and higher, it provides no benefit, as
366
464
GitPython solves that problem more robustly and safely by using the
367
465
``CREATE_NO_WINDOW`` process creation flag on Windows.
368
466
369
- Code that uses ``USE_SHELL = True`` or that passes ``shell=True`` to any GitPython
370
- functions should be updated to use the default value of ``False`` instead. ``True``
371
- is unsafe unless the effect of shell expansions is fully considered and accounted
372
- for, which is not possible under most circumstances.
467
+ Because Windows path search differs subtly based on whether a shell is used, in rare
468
+ cases changing this from ``True`` to ``False`` may keep an unusual git "executable",
469
+ such as a batch file, from being found. To fix this, set the command name or full
470
+ path in the :envvar:`GIT_PYTHON_GIT_EXECUTABLE` environment variable or pass the
471
+ full path to :func:`git.refresh` (or invoke the script using a ``.exe`` shim).
373
472
374
- See :
473
+ Further reading :
375
474
376
- - :meth:`Git.execute` (on the ``shell`` parameter).
377
- - https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
378
- - https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
475
+ * :meth:`Git.execute` (on the ``shell`` parameter).
476
+ * https://github.com/gitpython-developers/GitPython/commit/0d9390866f9ce42870d3116094cd49e0019a970a
477
+ * https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
478
+ * https://github.com/python/cpython/issues/91558#issuecomment-1100942950
479
+ * https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
379
480
"""
380
481
381
482
_git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE"
@@ -868,6 +969,11 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
868
969
self .cat_file_header : Union [None , TBD ] = None
869
970
self .cat_file_all : Union [None , TBD ] = None
870
971
972
+ def __getattribute__ (self , name : str ) -> Any :
973
+ if name == "USE_SHELL" :
974
+ _warn_use_shell (False )
975
+ return super ().__getattribute__ (name )
976
+
871
977
def __getattr__ (self , name : str ) -> Any :
872
978
"""A convenience method as it allows to call the command as if it was an object.
873
979
@@ -1138,7 +1244,12 @@ def execute(
1138
1244
1139
1245
stdout_sink = PIPE if with_stdout else getattr (subprocess , "DEVNULL" , None ) or open (os .devnull , "wb" )
1140
1246
if shell is None :
1141
- shell = self .USE_SHELL
1247
+ # Get the value of USE_SHELL with no deprecation warning. Do this without
1248
+ # warnings.catch_warnings, to avoid a race condition with application code
1249
+ # configuring warnings. The value could be looked up in type(self).__dict__
1250
+ # or Git.__dict__, but those can break under some circumstances. This works
1251
+ # the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1252
+ shell = super ().__getattribute__ ("USE_SHELL" )
1142
1253
_logger .debug (
1143
1254
"Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)" ,
1144
1255
redacted_command ,
0 commit comments